diff --git a/.github/workflows/sphinx.yaml b/.github/workflows/sphinx.yaml deleted file mode 100644 index 60011acbdcc931518cf6422f003f3ac562540ef3..0000000000000000000000000000000000000000 --- a/.github/workflows/sphinx.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: "Sphinx: render docs" - -on: - push: - branches: - - develop - -jobs: - docs: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - - name: Set up Python environment - uses: actions/setup-python@v4 - with: - python-version: 3.11 - - name: Install requirements - run: | - python3 -m pip install -r requirements.txt - python3 -m pip install -e . - - name: Build documentation - run: TZ=UTC sphinx-build -b html docs/source docs/build - - uses: peaceiris/actions-gh-pages@v3 - with: - publish_branch: gh-pages - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: docs/build/ - enable_jekyll: false diff --git a/.gitignore b/.gitignore index bce88d2bb8297e6a2fa4584b416e671fd5b91c8e..8fb4cb72eb86210a63a815add5dbc5aecc1da21d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,9 @@ __pycache__ -.coverage .idea .vscode .tox -coverage.xml *.egg-info -docs/build -docs/source/_static/openapi.json -docs/vale/styles/* -!docs/vale/styles/config/ venv/ .venv/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3bd2f9ead7bf919f2cd59793306dea1301130a18..8c0541a1b6898720caf198062cca4935595e9983 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,56 +1,7 @@ --- stages: - - tox - - documentation - - sonarqube - trigger_jenkins_build -include: - - docs/.gitlab-ci.yml - -#################################### tox - Testing and linting -run-tox-pipeline: - stage: tox - tags: - - docker-executor - image: python:3.11 - - # Change pip's cache directory to be inside the project directory since we can - # only cache local items. - variables: - PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" - - # Pip's cache doesn't store the python packages - # https://pip.pypa.io/en/stable/topics/caching/ - # - # If you want to also cache the installed packages, you have to install - # them in a virtualenv and cache it as well. - cache: - paths: - - .cache/pip - - before_script: - - pip install virtualenv - - virtualenv venv - - . venv/bin/activate - - pip install tox - - script: - - tox - - artifacts: - paths: - - htmlcov - - docs/source/_static/openapi.json - -sonarqube: - stage: sonarqube - image: sonarsource/sonar-scanner-cli - script: - - sonar-scanner -Dsonar.login=$SONAR_TOKEN -Dproject.settings=./sonar.properties - tags: - - docker-executor - trigger_jenkins_build: stage: trigger_jenkins_build image: alpine:latest diff --git a/Changelog.md b/Changelog.md index daf065ee50f1d1d82744c667f83bd44dc2641b2d..229d20d8505c4c605a775ca5b64f646ad606f77c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,8 @@ # Changelog All notable changes to this project will be documented in this file. +## [1.5] - 2024-03-12 +GAP Ansible collection version is pinned to 1.0.51 ## [1.4] - 2024-02-27 GAP Ansible collection version is pinned to 1.0.49 ## [1.3] - 2024-02-09 diff --git a/Dockerfile b/Dockerfile index c2abcb9550fee224a0814cfbb0e26c16de8d3a99..2b67b4042378fba83c121eb4b04d65f88d307025 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,23 @@ FROM python:3.11-alpine - -ARG ARTIFACT_VERSION +# We have to pin to 3.11 due to ncclient not supporting python 3.12 as of 2024-03-11 WORKDIR /app + +COPY ./requirements.txt ./requirements.txt COPY ./ansible-galaxy-requirements.yaml ./ansible-galaxy-requirements.yaml RUN apk add --update --no-cache gcc libc-dev libffi-dev curl vim bash openssh -RUN pip install \ - --pre \ - --trusted-host 150.254.211.2 \ - --extra-index-url https://150.254.211.2/artifactory/api/pypi/geant-swd-pypi/simple \ - goat-lso==${ARTIFACT_VERSION} +RUN pip install orchestrator-lso=="1.0.1" +RUN pip install -r requirements.txt + RUN ansible-galaxy install \ -r ansible-galaxy-requirements.yaml \ -p /app/gap/ansible RUN ansible-galaxy collection install \ -r ansible-galaxy-requirements.yaml \ -p /app/gap/ansible + EXPOSE 8000 ENTRYPOINT [] CMD ["python", "-m", "uvicorn", "lso.app:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/ansible-galaxy-requirements.yaml b/ansible-galaxy-requirements.yaml index ea0bd0be80dc09ef97df75c2c62e2215c739f50e..1fa152fc7f681b7015379b225130b67d178b3030 100644 --- a/ansible-galaxy-requirements.yaml +++ b/ansible-galaxy-requirements.yaml @@ -3,6 +3,6 @@ collections: - juniper.device - junipernetworks.junos - name: geant.gap_ansible - version: 1.0.49 + version: 1.0.51 roles: - Juniper.junos diff --git a/build-docs.sh b/build-docs.sh deleted file mode 100755 index 9ac7997e286c2dbd0073f597c323c80de11eb992..0000000000000000000000000000000000000000 --- a/build-docs.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -set -o errexit -set -o nounset - -pip install -r requirements.txt -pip install -e . - -rm -r ./docs/build/* -vale --config=docs/vale/.vale.ini sync -vale --config=docs/vale/.vale.ini docs/source/*.rst lso/*.py - -sphinx-build -b html docs/source docs/build diff --git a/docs/.gitlab-ci.yml b/docs/.gitlab-ci.yml deleted file mode 100644 index 17e02529afc03a7e08b86014b109fb0ce761b372..0000000000000000000000000000000000000000 --- a/docs/.gitlab-ci.yml +++ /dev/null @@ -1,19 +0,0 @@ ---- -##### Vale - Documentation linter -lint-documentation: - stage: documentation - image: - name: jdkato/vale:latest - entrypoint: [""] - - tags: - - docker-executor - needs: - - job: run-tox-pipeline # Only run when tox has finished - - before_script: - - cd $CI_PROJECT_DIR/docs/vale - - vale sync - - script: - - vale $CI_PROJECT_DIR/docs/source/*.rst $CI_PROJECT_DIR/lso/*.py diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d0c3cbf1020d5c292abdedf27627c6abe25e2293..0000000000000000000000000000000000000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css deleted file mode 100644 index af9611171b13d53584f414daf47c984b07f84719..0000000000000000000000000000000000000000 --- a/docs/source/_static/custom.css +++ /dev/null @@ -1,13 +0,0 @@ -.wy-menu > p > span { - color: rgb(237 21 86); -} - -section > dl > .sig-object { - background: #e5e8e8 !important; - color: rgb(237 21 86) !important; - border-top: 3px solid rgb(167 179 180) !important; -} - -.code.literal { - color: rgb(226 67 1) !important; -} diff --git a/docs/source/_static/geant_logo_white.svg b/docs/source/_static/geant_logo_white.svg deleted file mode 100644 index 31bfead62b052812096b5a9c79cf782ce330a87c..0000000000000000000000000000000000000000 --- a/docs/source/_static/geant_logo_white.svg +++ /dev/null @@ -1,16 +0,0 @@ -<svg width="79" height="35" viewBox="0 0 79 35" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_101_15)"> -<path d="M15.9 17.8C16.5 17.3 17 17.1 17.4 17.1C18.4 17.1 18.7 17.8 18.7 18.2C18.5 18.3 14.2 19.7 14 19.8C13.9 19.7 13.9 19.6 13.8 19.6C14 19.4 15.9 17.8 15.9 17.8Z" fill="white" fill-opacity="0.85"/> -<path d="M0 27C0 31.8 2.1 34.3 6.3 34.3C9.1 34.3 10.8 33.1 10.8 33L10.9 32.9V26.2H5.2V28.1C5.2 28.1 8.1 28.1 8.6 28.1C8.6 28.6 8.6 31.6 8.6 31.9C8.3 32.1 7.4 32.4 6.2 32.4C3.7 32.4 2.5 30.6 2.5 27C2.5 24.9 3.1 22.4 5.9 22.4C7.8 22.4 8.5 23.5 8.5 24.5V24.8H11.1V24.5C11.1 22.1 9 20.5 5.9 20.5C2.2 20.5 0 22.9 0 27Z" fill="white" fill-opacity="0.85"/> -<path d="M20.2 20.7H12.6V34.1H20.7V32.2C20.7 32.2 15.5 32.2 14.9 32.2C14.9 31.7 14.9 28.5 14.9 28C15.5 28 20.2 28 20.2 28V26.1C20.2 26.1 15.5 26.1 14.9 26.1C14.9 25.6 14.9 23 14.9 22.5C15.5 22.5 20.5 22.5 20.5 22.5V20.6H20.2V20.7Z" fill="white" fill-opacity="0.85"/> -<path d="M54.5 20.7H42.9C42.9 20.7 42.9 28.7 42.9 30.6C42 29 37.2 20.7 37.2 20.7H34.5V34.1H36.8C36.8 34.1 36.8 26.1 36.8 24.2C37.7 25.8 42.5 34.1 42.5 34.1H45.2C45.2 34.1 45.2 23.2 45.2 22.6C45.7 22.6 48.4 22.6 48.9 22.6C48.9 23.2 48.9 34.1 48.9 34.1H51.3C51.3 34.1 51.3 23.2 51.3 22.6C51.8 22.6 54.9 22.6 54.9 22.6V20.7H54.5V20.7Z" fill="white" fill-opacity="0.85"/> -<path d="M28.9 20.7H28.7H26.4L21.4 34.1H23.8C23.8 34.1 25.1 30.6 25.3 30.2C25.7 30.2 29.8 30.2 30.2 30.2C30.3 30.6 31.7 34.1 31.7 34.1H34L28.9 20.7ZM25.9 28.3C26.1 27.6 27.3 24.4 27.7 23.3C28.1 24.4 29.2 27.6 29.5 28.3C28.7 28.3 26.6 28.3 25.9 28.3Z" fill="white" fill-opacity="0.85"/> -<path d="M77 8C68.2 -2.9 32.6 12.5 23 16.5C22.3 16.8 21.4 16.7 20.9 15.7C21.3 16.7 22.1 17.1 23.1 16.7C35.8 11.6 66.5 0.600002 74.2 10.7C77.7 15.3 76.7 20.9 72.9 28.8C72.7 29.1 72.6 29.4 72.6 29.4C72.6 29.4 72.6 29.4 72.6 29.5C72.6 29.5 72.6 29.5 72.6 29.6C72.3 30.1 71.9 30.3 71.6 30.4C72 30.4 72.5 30.2 72.9 29.6C73 29.5 73.1 29.3 73.3 29C78.7 19.5 80.7 12.5 77 8Z" fill="white" fill-opacity="0.85"/> -<path d="M70.3 29.9C70.2 29.8 68.6 28.4 67 26.9C58.7 19 33.4 -5.3 22.4 1.1C19.3 2.9 18.8 8.2 20.7 15.2C20.7 15.3 20.8 15.4 20.8 15.5C21 16.2 21.5 16.7 22.2 16.7C21.7 16.6 21.3 16.2 21.1 15.7C21.1 15.6 21 15.5 21 15.5C21 15.4 20.9 15.3 20.9 15.1C20.9 15 20.9 14.9 20.8 14.9C19.8 9 21 5.1 23.4 3.5C32.3 -2.5 53.5 15.8 64.2 25C66.6 27.1 69.4 29.5 70.2 30.1C71.4 31 72.4 30 72.7 29.5C72.3 30.1 71.3 30.7 70.3 29.9Z" fill="white" fill-opacity="0.85"/> -</g> -<defs> -<clipPath id="clip0_101_15"> -<rect width="78.9" height="34.3" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index c937decfb950fe541f11c53e1ea9a882e3b1b3b0..0000000000000000000000000000000000000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,78 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -from importlib import import_module -from docutils.parsers.rst import Directive -from docutils import nodes -from sphinx import addnodes -import json -import os -import sys - -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "lso"))) - - -class RenderAsJSON(Directive): - # cf. https://stackoverflow.com/a/59883833 - - required_arguments = 1 - - def run(self): - module_path, member_name = self.arguments[0].rsplit(".", 1) - - member_data = getattr(import_module(module_path), member_name) - code = json.dumps(member_data, indent=2) - - literal = nodes.literal_block(code, code) - literal["language"] = "json" - - return [ - addnodes.desc_name(text=member_name), - addnodes.desc_content("", literal) - ] - - -def setup(app): - app.add_directive("asjson", RenderAsJSON) - - -# -- Project information ----------------------------------------------------- - -project = "Lightweight Service Orchestrator" -copyright = "2023, GÉANT Vereniging" -author = "GÉANT Orchestration & Automation Team" - -# -- General configuration --------------------------------------------------- - -extensions = [ - "sphinx_rtd_theme", - "sphinx.ext.autodoc", - "sphinx.ext.coverage", - "sphinx.ext.todo" -] - -templates_path = ["templates"] -exclude_patterns = [] - -# -- Options for HTML output ------------------------------------------------- - -html_theme = "sphinx_rtd_theme" -html_static_path = ["_static"] -html_theme_options = {"style_nav_header_background": "rgb(0 63 95)"} -html_css_files = ["custom.css"] -html_logo = "_static/geant_logo_white.svg" - - -# Both the class' and the ``__init__`` method's docstring are concatenated and inserted. -autoclass_content = "both" -autodoc_typehints = "none" - -# Display todos by setting to True -todo_include_todos = True diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index 9e5c505600059c4517dcb99793126e1558fbac2f..0000000000000000000000000000000000000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Lightweight Service Orchestrator -================================ - -Documentation for LSO (Lightweight Service Orchestrator). - -.. toctree:: - :maxdepth: 1 - :caption: Contents: - - quickstart - modules diff --git a/docs/source/module/config.rst b/docs/source/module/config.rst deleted file mode 100644 index feabe0956cb0994ff2dfcb1d29eb830fdce52271..0000000000000000000000000000000000000000 --- a/docs/source/module/config.rst +++ /dev/null @@ -1,6 +0,0 @@ -``lso.config`` -============== - -.. automodule:: lso.config - :members: - :show-inheritance: diff --git a/docs/source/module/playbook.rst b/docs/source/module/playbook.rst deleted file mode 100644 index 7a9bd432cfaae15534ce8f4123669275b7a0f757..0000000000000000000000000000000000000000 --- a/docs/source/module/playbook.rst +++ /dev/null @@ -1,7 +0,0 @@ -``lso.playbook`` -================ - -.. automodule:: lso.playbook - :members: - :private-members: - :show-inheritance: diff --git a/docs/source/module/routes/default.rst b/docs/source/module/routes/default.rst deleted file mode 100644 index af64afb2b28c16f46075ee3ba8bffe10749abd9d..0000000000000000000000000000000000000000 --- a/docs/source/module/routes/default.rst +++ /dev/null @@ -1,6 +0,0 @@ -``lso.routes.default`` -====================== - -.. automodule:: lso.routes.default - :members: - :show-inheritance: diff --git a/docs/source/module/routes/index.rst b/docs/source/module/routes/index.rst deleted file mode 100644 index 12555477a7e5f22aeedb7d6eb9062a155034b335..0000000000000000000000000000000000000000 --- a/docs/source/module/routes/index.rst +++ /dev/null @@ -1,16 +0,0 @@ -``lso.routes`` -============== - -.. automodule:: lso.routes - :members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - :maxdepth: 2 - :titlesonly: - - default - playbook diff --git a/docs/source/module/routes/playbook.rst b/docs/source/module/routes/playbook.rst deleted file mode 100644 index 7a3da3c57df5828d47cd5be6630214ccafd9ca19..0000000000000000000000000000000000000000 --- a/docs/source/module/routes/playbook.rst +++ /dev/null @@ -1,6 +0,0 @@ -``lso.routes.playbook`` -======================= - -.. automodule:: lso.routes.playbook - :members: - :show-inheritance: diff --git a/docs/source/modules.rst b/docs/source/modules.rst deleted file mode 100644 index 1455bfc7dbfb42a7ab16c31c913ccc497b334b48..0000000000000000000000000000000000000000 --- a/docs/source/modules.rst +++ /dev/null @@ -1,31 +0,0 @@ -========================= -Sub-packages and -modules -========================= - -This page lists references to the documentation of all sub-packages and -modules. - -Subpackages ------------ - -.. toctree:: - :maxdepth: 2 - :titlesonly: - - module/routes/index - -Submodules ----------- - -.. toctree:: - :maxdepth: 2 - :titlesonly: - - module/config - module/playbook - - -========== - -.. automodule:: lso - :members: - :show-inheritance: diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst deleted file mode 100644 index f9ae6b71974dd6dec51026eef50b2e00f2fe2176..0000000000000000000000000000000000000000 --- a/docs/source/quickstart.rst +++ /dev/null @@ -1,86 +0,0 @@ -Quick start -=========== - -This is a quick setup guide for running standalone on your local machine. - -As a Docker container ---------------------- -To run LSO as a Docker container, build an image using the ``Dockerfile`` as an example. Be sure to update -``ansible-galaxy-requirements.yaml`` accordingly, depending on your specific Ansible collection and -role needs. - -An example Docker compose file is presented below: - -.. code-block:: yaml - - version: "3.5" - services: - lso: - image: goat-lso:$LSO_VERSION_TAG - environment: - SETTINGS_FILENAME: /app/config.json - ANSIBLE_ROLES_PATH: /app/lso/ansible_roles - volumes: - - "/home/user/config.json:/app/config.json:ro" - - "/home/user/ansible_inventory:/opt/ansible_inventory:ro" - - "~/.ssh/id_ed25519.pub:/root/.ssh/id_rsa.pub:ro" - - "~/.ssh/id_ed25519:/root/.ssh/id_rsa:ro" - restart: unless-stopped - -This will expose the API on port 8080. The container requires some more files to be mounted: - -* A ``config.json`` that references to the location where the Ansible playbooks are stored **inside the container**. -* An Ansible inventory for all host and group variables that are used in the playbooks -* A public/private key pair for SSH authentication on external machines that are targeted by Ansible playbooks. -* Any Ansible-specific configuration (such as ``collections_path``, ``roles_path``, etc.) should be set using - environment variables. ``ANSIBLE_ROLES_PATH`` is given as an example in the Docker compose snippet above. - -Install the module ------------------- - -As an alternative, below are a set of instructions for installing and running LSO directly on a machine. - -*One of these should be what you're looking for:* - -* Install the latest module snapshot - -.. code-block:: bash - - python3 -m venv my-venv-directory - . my-venv-directory/bin/activate - - pip install --pre --extra-index-url https://artifactory.software.geant.org/artifactory/api/pypi/geant-swd-pypi/simple goat-lso - -* Install the source code - -.. code-block:: bash - - python3 -m venv my-venv-directory - . my-venv-directory/bin/activate - - git clone https://gitlab.software.geant.org/goat/gap/lso.git - cd lso - pip install -e . - - # for a full dev environment - pip install -r requirements.txt - -Running the app ---------------- - -* Create a settings file, see ``config.json.example`` for an example. -* If necessary, set the environment variable ``ANSIBLE_HOME`` to a custom path. -* Run the app like this (``app.py`` starts the server on port 44444): - - .. code-block:: bash - - SETTINGS_FILENAME=/absolute/path/to/config.json python -m lso.app - -Examples - -* Get the version - - .. code-block:: bash - - curl http://localhost:44444/api/version - -* View the docs by loading http://localhost:44444/docs in your browser diff --git a/docs/vale/.vale.ini b/docs/vale/.vale.ini deleted file mode 100644 index 36aa7e637a8cc4c58fbdd9b5315ed617b4ff3dda..0000000000000000000000000000000000000000 --- a/docs/vale/.vale.ini +++ /dev/null @@ -1,26 +0,0 @@ -StylesPath = styles - -MinAlertLevel = suggestion - -Vocab = geant-jargon, Sphinx - -Packages = proselint, Microsoft - -[*] -BasedOnStyles = Vale, proselint, Microsoft -; Found to be too intrusive -Microsoft.Passive = NO -; We are not a general audience -Microsoft.GeneralURL = NO -; It's okay to leave TODOs in the code, that's what they're for -proselint.Annotations = NO -; Replacing a ... with … shouldn't be holding back the entire CI pipeline -proselint.Typography = warning -; Same applies for not using contractions -Microsoft.Contractions = warning -Microsoft.Headings = NO - -TokenIgnores = (:class:`\S+`) - -[formats] -py = rst diff --git a/docs/vale/styles/config/vocabularies/Sphinx/accept.txt b/docs/vale/styles/config/vocabularies/Sphinx/accept.txt deleted file mode 100644 index 0a57a0fe336bf64acee73b3caaf5b58ba4e3fa61..0000000000000000000000000000000000000000 --- a/docs/vale/styles/config/vocabularies/Sphinx/accept.txt +++ /dev/null @@ -1 +0,0 @@ -param diff --git a/docs/vale/styles/config/vocabularies/geant-jargon/accept.txt b/docs/vale/styles/config/vocabularies/geant-jargon/accept.txt deleted file mode 100644 index db8478e9e0bcbc52f20fa666cc492cdaf9a75d2c..0000000000000000000000000000000000000000 --- a/docs/vale/styles/config/vocabularies/geant-jargon/accept.txt +++ /dev/null @@ -1,4 +0,0 @@ -[LSO|lso] -[Ss]ubpackages -Vereniging -against diff --git a/lso/__init__.py b/lso/__init__.py deleted file mode 100644 index 2d752ef48be81ce5eeba3563f3497f9de9a5fa56..0000000000000000000000000000000000000000 --- a/lso/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Automatically invoked app factory.""" - -import logging - -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware - -from lso import config, environment -from lso.routes.default import router as default_router -from lso.routes.playbook import router as playbook_router - - -def create_app() -> FastAPI: - """Override default settings with those found in the file read from environment variable `SETTINGS_FILENAME`. - - :return: a new flask app instance - """ - app = FastAPI() - - app.add_middleware( - CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"] - ) - - app.include_router(default_router, prefix="/api") - app.include_router(playbook_router, prefix="/api/playbook") - - # test that config params are loaded and available - config.load() - - environment.setup_logging() - - logging.info("FastAPI app initialized") - - return app diff --git a/lso/app.py b/lso/app.py deleted file mode 100644 index 8ca6862c01e8e596b67ae3bac94f45599d5e5a52..0000000000000000000000000000000000000000 --- a/lso/app.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Default app creation.""" - -import lso - -app = lso.create_app() - -if __name__ == "__main__": - import uvicorn - - uvicorn.run("lso.app:app", host="0.0.0.0", port=44444, log_level="debug") diff --git a/lso/config.py b/lso/config.py deleted file mode 100644 index ddc1781cc5c184715265094970653867e659cbda..0000000000000000000000000000000000000000 --- a/lso/config.py +++ /dev/null @@ -1,57 +0,0 @@ -"""A module for loading configuration data, including a config schema that data is validated against. - -Data is loaded from a file, the location of which may be specified when using :func:`load_from_file`. -Config file location can also be loaded from environment variable ``$SETTINGS_FILENAME``, which is default behaviour in -:func:`load`. -""" - -import json -import os -from pathlib import Path - -import jsonschema -from pydantic import BaseModel - -CONFIG_SCHEMA = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": {"ansible_playbooks_root_dir": {"type": "string"}}, - "required": ["ansible_playbooks_root_dir"], - "additionalProperties": False, -} -DEFAULT_REQUEST_TIMEOUT = 10 - - -class Config(BaseModel): - """Simple Config class. - - Contains the root directory at which Ansible playbooks are present. - """ - - ansible_playbooks_root_dir: str - - -def load_from_file(file: Path) -> Config: - """Load, validate and return configuration parameters. - - Input is validated against this jsonschema: - - .. asjson:: lso.config.CONFIG_SCHEMA - - :param file: :class:`Path` object that produces the config file. - :return: a dict containing the parsed configuration parameters. - """ - config = json.loads(file.read_text()) - jsonschema.validate(config, CONFIG_SCHEMA) - return Config(**config) - - -def load() -> Config: - """Load a config file, located at the path specified in the environment variable ``$SETTINGS_FILENAME``. - - Loading and validating the file is performed by :func:`load_from_file`. - - :return: a dict containing the parsed configuration parameters - """ - assert "SETTINGS_FILENAME" in os.environ, "Environment variable SETTINGS_FILENAME not set" # noqa: S101 - return load_from_file(Path(os.environ["SETTINGS_FILENAME"])) diff --git a/lso/environment.py b/lso/environment.py deleted file mode 100644 index f50d645165a50170b47eae5d47200b7e5de129da..0000000000000000000000000000000000000000 --- a/lso/environment.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Environment module for setting up logging.""" - -import json -import logging.config -import os -from pathlib import Path - -LOGGING_DEFAULT_CONFIG = { - "version": 1, - "disable_existing_loggers": False, - "formatters": {"simple": {"format": "%(asctime)s - %(name)s (%(lineno)d) - %(levelname)s - %(message)s"}}, - "handlers": { - "console": { - "class": "logging.StreamHandler", - "level": "DEBUG", - "formatter": "simple", - "stream": "ext://sys.stdout", - } - }, - "loggers": {"resource_management": {"level": "DEBUG", "handlers": ["console"], "propagate": False}}, - "root": {"level": "INFO", "handlers": ["console"]}, -} - - -def setup_logging() -> None: - """Set up logging using the configured filename. - - If LOGGING_CONFIG is defined in the environment, use this for the filename, otherwise use LOGGING_DEFAULT_CONFIG. - """ - logging_config = LOGGING_DEFAULT_CONFIG - if "LOGGING_CONFIG" in os.environ: - filename = os.environ["LOGGING_CONFIG"] - config_file = Path(filename).read_text() - logging_config = json.loads(config_file) - - logging.config.dictConfig(logging_config) diff --git a/lso/playbook.py b/lso/playbook.py deleted file mode 100644 index 32155c5b0f8d148528154e02787ba59e37d969dd..0000000000000000000000000000000000000000 --- a/lso/playbook.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Module that gathers common API responses and data models.""" - -import logging -import threading -import uuid -from pathlib import Path -from typing import Any - -import ansible_runner -import requests -from fastapi import status -from fastapi.responses import JSONResponse -from pydantic import HttpUrl - -from lso import config -from lso.config import DEFAULT_REQUEST_TIMEOUT - -logger = logging.getLogger(__name__) - - -def get_playbook_path(playbook_name: str) -> Path: - """Get the path of a playbook on the local filesystem.""" - config_params = config.load() - return Path(config_params.ansible_playbooks_root_dir) / playbook_name - - -def playbook_launch_success(job_id: str) -> JSONResponse: - """Return a :class:`PlaybookLaunchResponse` for the successful start of a playbook execution. - - :return JSONResponse: A playbook launch response that's successful. - """ - return JSONResponse(content={"job_id": job_id}, status_code=status.HTTP_201_CREATED) - - -def playbook_launch_error(reason: str, status_code: int = status.HTTP_400_BAD_REQUEST) -> JSONResponse: - """Return a :class:`PlaybookLaunchResponse` for the erroneous start of a playbook execution. - - :param str reason: The reason why a request has failed. - :param status status_code: The HTTP status code that should be associated with this request. Defaults to HTTP 400: - Bad request. - :return JSONResponse: A playbook launch response that's unsuccessful. - """ - return JSONResponse(content={"error": reason}, status_code=status_code) - - -def _run_playbook_proc( - job_id: str, playbook_path: str, extra_vars: dict, inventory: dict[str, Any] | str, callback: str -) -> None: - """Run a playbook, internal function. - - :param str job_id: Identifier of the job that's executed. - :param str playbook_path: Ansible playbook to be executed. - :param dict extra_vars: Extra variables passed to the Ansible playbook. - :param str callback: Callback URL to return output to when execution is completed. - :param dict[str, Any] | str inventory: Ansible inventory to run the playbook against. - """ - ansible_playbook_run = ansible_runner.run(playbook=playbook_path, inventory=inventory, extravars=extra_vars) - - payload = { - "status": ansible_playbook_run.status, - "job_id": job_id, - "output": ansible_playbook_run.stdout.readlines(), - "return_code": int(ansible_playbook_run.rc), - } - - request_result = requests.post(callback, json=payload, timeout=DEFAULT_REQUEST_TIMEOUT) - if not status.HTTP_200_OK <= request_result.status_code < status.HTTP_300_MULTIPLE_CHOICES: - msg = f"Callback failed: {request_result.text}" - logger.error(msg) - - -def run_playbook( - playbook_path: Path, - extra_vars: dict[str, Any], - inventory: dict[str, Any] | str, - callback: HttpUrl, -) -> JSONResponse: - """Run an Ansible playbook against a specified inventory. - - :param Path playbook_path: playbook to be executed. - :param dict[str, Any] extra_vars: Any extra vars needed for the playbook to run. - :param dict[str, Any] | str inventory: The inventory that the playbook is executed against. - :param HttpUrl callback: Callback URL where the playbook should send a status update when execution is completed. - This is used for workflow-orchestrator to continue with the next step in a workflow. - :return: Result of playbook launch, this could either be successful or unsuccessful. - :rtype: :class:`fastapi.responses.JSONResponse` - """ - if not Path.exists(playbook_path): - msg = f"Filename '{playbook_path}' does not exist." - return playbook_launch_error(reason=msg, status_code=status.HTTP_404_NOT_FOUND) - - if not ansible_runner.utils.isinventory(inventory): - msg = "Invalid inventory provided. Should be a string, or JSON object." - return playbook_launch_error(reason=msg, status_code=status.HTTP_400_BAD_REQUEST) - - job_id = str(uuid.uuid4()) - thread = threading.Thread( - target=_run_playbook_proc, - kwargs={ - "job_id": job_id, - "playbook_path": str(playbook_path), - "inventory": inventory, - "extra_vars": extra_vars, - "callback": callback, - }, - ) - thread.start() - - return playbook_launch_success(job_id=job_id) diff --git a/lso/routes/__init__.py b/lso/routes/__init__.py deleted file mode 100644 index 6df1dee1f6483ed379ac3d7f753f8b8a654b3928..0000000000000000000000000000000000000000 --- a/lso/routes/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Module of all routes that are available in LSO.""" diff --git a/lso/routes/default.py b/lso/routes/default.py deleted file mode 100644 index d23f5a1370eda8cacb930ff08ac1f1111450207f..0000000000000000000000000000000000000000 --- a/lso/routes/default.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Default route located at the root URL /. - -For now only includes a single endpoint that responds with the current version of the API and LSO. -""" - -from importlib import metadata - -from fastapi import APIRouter -from pydantic import BaseModel, constr - -API_VERSION = "1.0" -VersionString = constr(pattern=r"\d+\.\d+") - -router = APIRouter() - - -class Version(BaseModel): - """Simple model for returning a version number of both the API and the `goat-lso` module.""" - - api: VersionString # type: ignore[valid-type] - module: VersionString # type: ignore[valid-type] - - -@router.get("/version") -def version() -> Version: - """Return the version numbers of the API version, and the module version. - - :return: Version object with both API and `goat-lso` versions numbers. - """ - return Version(api=API_VERSION, module=metadata.version("goat-lso")) diff --git a/lso/routes/playbook.py b/lso/routes/playbook.py deleted file mode 100644 index 43808874035d247e2fb5b38270807f63a6ca9d5d..0000000000000000000000000000000000000000 --- a/lso/routes/playbook.py +++ /dev/null @@ -1,78 +0,0 @@ -"""The API endpoint from which Ansible playbooks can be executed.""" - -import json -import tempfile -from contextlib import redirect_stderr -from io import StringIO -from typing import Annotated, Any - -from ansible.inventory.manager import InventoryManager -from ansible.parsing.dataloader import DataLoader -from fastapi import APIRouter, HTTPException, status -from fastapi.responses import JSONResponse -from pydantic import AfterValidator, BaseModel, HttpUrl - -from lso.playbook import get_playbook_path, run_playbook - -router = APIRouter() - - -def _inventory_validator(inventory: dict[str, Any] | str) -> dict[str, Any] | str: - """Validate the format of the provided inventory by trying to parse it. - - If an inventory cannot be parsed without warnings or errors, these are returned to the user by means of an HTTP - status 422 for 'unprocessable entity'. - """ - loader = DataLoader() - output = StringIO() - with tempfile.NamedTemporaryFile(mode="w+") as temp_inv, redirect_stderr(output): - json.dump(inventory, temp_inv, ensure_ascii=False) - temp_inv.flush() - - inventory_manager = InventoryManager(loader=loader, sources=[temp_inv.name], parse=True) - inventory_manager.parse_source(temp_inv.name) - - output.seek(0) - error_messages = output.readlines() - if error_messages: - raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=error_messages) - - return inventory - - -PlaybookInventory = Annotated[dict[str, Any] | str, AfterValidator(_inventory_validator)] - - -class PlaybookRunParams(BaseModel): - """Parameters for executing an Ansible playbook.""" - - #: The filename of a playbook that is executed. It should be present inside the directory defined in the - #: configuration option ``ansible_playbooks_root_dir``. - playbook_name: str - #: The address where LSO should call back to upon completion. - callback: HttpUrl - #: The inventory to run the playbook against. This inventory can also include any host vars, if needed. When - #: including host vars, it should be a dictionary. Can be a simple string containing hostnames when no host vars are - #: needed. In the latter case, multiple hosts should be separated with a ``\n`` newline character only. - inventory: PlaybookInventory - #: Extra variables that should get passed to the playbook. This includes any required configuration objects - #: from the workflow orchestrator, commit comments, whether this execution should be a dry run, a trouble ticket - #: number, etc. Which extra vars are required solely depends on what inputs the playbook requires. - extra_vars: dict[str, Any] = {} - - -@router.post("/") -def run_playbook_endpoint(params: PlaybookRunParams) -> JSONResponse: - """Launch an Ansible playbook to modify or deploy a subscription instance. - - The response will contain either a job ID, or error information. - - :param PlaybookRunParams params: Parameters for executing a playbook. - :return JSONResponse: Response from the Ansible runner, including a run ID. - """ - return run_playbook( - playbook_path=get_playbook_path(params.playbook_name), - extra_vars=params.extra_vars, - inventory=params.inventory, - callback=params.callback, - ) diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 2871c77bd484013bc33e1bebb5523a63001ce57c..0000000000000000000000000000000000000000 --- a/pyproject.toml +++ /dev/null @@ -1,105 +0,0 @@ -[tool.mypy] -exclude = [ - "venv", - "test/*", - "docs" -] -ignore_missing_imports = true -disallow_untyped_calls = true -disallow_untyped_defs = true -disallow_incomplete_defs = true -disallow_untyped_decorators = true -no_implicit_optional = true -strict_optional = true -namespace_packages = true -warn_unused_ignores = true -warn_redundant_casts = true -warn_no_return = true -warn_unreachable = true -implicit_reexport = false -strict_equality = true -show_error_codes = true -show_column_numbers = true -# Suppress "note: By default the bodies of untyped functions are not checked" -disable_error_code = "annotation-unchecked" -# Forbid the use of a generic "type: ignore" without specifying the exact error that is ignored -enable_error_code = "ignore-without-code" - -[tool.ruff] -extend-exclude = [ - "htmlcov", - "docs", -] -ignore = [ - "COM812", - "D203", - "D213", - "N805", - "PLR0913", - "PLR0904", - "PLW1514", - "S104" -] -line-length = 120 -select = [ - "A", - "ARG", - "B", - "BLE", - "C", - "COM", - "C4", - "C90", - "D", - "DTZ", - "E", - "EM", - "ERA", - "F", - "FA", - "FBT", - "FLY", - "FURB", - "G", - "I", - "ICN", - "INP", - "ISC", - "LOG", - "N", - "PERF", - "PGH", - "PIE", - "PL", - "PT", - "PTH", - "PYI", - "Q", - "RET", - "R", - "RET", - "RSE", - "RUF", - "S", - "SIM", - "SLF", - "T", - "T20", - "TID", - "TRY", - "UP", - "W", - "YTT" -] -target-version = "py311" - -[tool.ruff.flake8-tidy-imports] -ban-relative-imports = "all" - -[tool.ruff.per-file-ignores] -"test/*" = ["D", "S101"] -"setup.py" = ["D100"] - -[tool.ruff.isort] -known-third-party = ["pydantic", "migrations"] -known-first-party = ["test", "docs"] diff --git a/requirements.txt b/requirements.txt index aa526ec315c1012b30d0d029f676acea6ce468fe..9b6bf6ec29352159d76903b0bffdc1b0d91ce8d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,9 @@ -pytest~=7.4.3 -Faker~=20.0.3 -responses~=0.24.1 -sphinx~=7.2.6 -sphinx-rtd-theme~=1.3.0 -docutils~=0.18.1 -mypy~=1.7.0 -ruff~=0.1.6 -types-setuptools~=68.2.0.1 -types-requests~=2.31.0.10 +ansible_merge_vars~=5.0.0 +jinja2==3.1.2 +jmespath~=1.0.1 +junos-eznc~=2.6.8 +jxmlease~=1.0.3 +ncclient~=0.6.13 +netaddr~=0.8.0 +requests~=2.31.0 +ruamel.yaml~=0.18.5 diff --git a/setup.py b/setup.py index 8b3ec962cc70b987e05c65b0c246de763150242b..bac1abcfc2da501d3417ddbac11ee3ee28b2ac74 100644 --- a/setup.py +++ b/setup.py @@ -1,44 +1,15 @@ -from setuptools import find_packages, setup +from setuptools import setup setup( name="goat-lso", - version="1.4", + version="1.5", author="GÉANT Orchestration & Automation Team", author_email="goat@geant.org", description="Lightweight Service Orchestrator", url="https://gitlab.software.geant.org/goat/gap/lso", - packages=find_packages(), - install_requires=[ - "ansible_merge_vars~=5.0.0", - "ansible-runner~=2.3.4", - "ansible~=8.6.1", - "dictdiffer~=0.9.0", - "fastapi~=0.104.1", - "GitPython~=3.1.40", - "httpx~=0.25.1", - "jinja2==3.1.2", - "jmespath~=1.0.1", - "jsonschema~=4.20.0", - "junos-eznc~=2.6.8", - "jxmlease~=1.0.3", - "ncclient~=0.6.13", - "netaddr~=0.8.0", - "pydantic~=2.0.3", - "requests~=2.31.0", - "ruamel.yaml~=0.18.5", - "uvicorn[standard]~=0.22.0", - "xmltodict~=0.13.0", - ], + packages=[], + install_requires=[], license="MIT", license_files=("LICENSE.txt",), - classifiers=[ - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Development Status :: 3 - Alpha", - "Framework :: FastAPI", - "Intended Audience :: System Administrators", - "Intended Audience :: Telecommunications Industry", - ], + classifiers=[], ) diff --git a/sonar.properties b/sonar.properties deleted file mode 100644 index 340ffdfee3dbc790e2f29449ac2744aba19e1f9f..0000000000000000000000000000000000000000 --- a/sonar.properties +++ /dev/null @@ -1,6 +0,0 @@ -sonar.projectKey=lso -sonar.projectName='Lightweight Service Orchestrator' -sonar.projectVersion=1.0 -sonar.sources=lso -sonar.python.coverage.reportPaths=coverage.xml -sonar.host.url=https://sonarqube.software.geant.org/ diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/test/conftest.py b/test/conftest.py deleted file mode 100644 index 09c0b82478f9f657c4cc1e6c11928585a0914fa7..0000000000000000000000000000000000000000 --- a/test/conftest.py +++ /dev/null @@ -1,62 +0,0 @@ -import json -import os -import tempfile -from collections.abc import Callable, Generator -from io import StringIO -from pathlib import Path -from typing import Any - -import pytest -from faker import Faker -from fastapi.testclient import TestClient - -import lso - - -@pytest.fixture() -def mocked_ansible_runner_run() -> Callable: - class Runner: - def __init__(self) -> None: - self.status = "success" - self.rc = 0 - self.stdout = StringIO("[{'step one': 'results'}, {'step two': 2}]") - - def run(*args: Any, **kwargs: Any) -> Runner: # noqa: ARG001 - return Runner() - - return run - - -@pytest.fixture(scope="session") -def configuration_data() -> dict[str, str]: - """Start the server with valid configuration data.""" - with tempfile.TemporaryDirectory() as tempdir: - # Create required YAML files for the unit tests - (Path(tempdir) / "placeholder.yaml").touch() - - yield {"ansible_playbooks_root_dir": tempdir} - - -@pytest.fixture(scope="session") -def data_config_filename(configuration_data: dict[str, str]) -> Generator[str, Any, None]: - """Fixture that will yield a filename that contains a valid configuration. - - :return: Path to valid configuration file - """ - with tempfile.NamedTemporaryFile(mode="w") as file: - file.write(json.dumps(configuration_data)) - file.flush() - yield file.name - - -@pytest.fixture(scope="session") -def client(data_config_filename: str) -> TestClient: - """Return a client that can be used to test the server.""" - os.environ["SETTINGS_FILENAME"] = data_config_filename - app = lso.create_app() - return TestClient(app) # wait here until calling context ends - - -@pytest.fixture(scope="session") -def faker() -> Faker: - return Faker(locale="en_GB") diff --git a/test/routes/__init__.py b/test/routes/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/test/routes/test_default.py b/test/routes/test_default.py deleted file mode 100644 index 3d88dfb3c0c1e41db4eca2306d0ed8c9f15ab7bf..0000000000000000000000000000000000000000 --- a/test/routes/test_default.py +++ /dev/null @@ -1,20 +0,0 @@ -from importlib import metadata - -import jsonschema -import responses -from fastapi import status -from starlette.testclient import TestClient - -from lso.routes.default import API_VERSION, Version - - -@responses.activate -def test_ip_trunk_modification(client: TestClient) -> None: - rv = client.get("/api/version/") - assert rv.status_code == status.HTTP_200_OK, rv.text - response = rv.json() - - jsonschema.validate(response, Version.model_json_schema()) - - assert response["api"] == API_VERSION, response["api"] - assert response["module"] == metadata.version("goat-lso"), response["module"] diff --git a/test/routes/test_playbook.py b/test/routes/test_playbook.py deleted file mode 100644 index 84391821f3543496363434bdea24764fef1789fc..0000000000000000000000000000000000000000 --- a/test/routes/test_playbook.py +++ /dev/null @@ -1,107 +0,0 @@ -import re -import time -from collections.abc import Callable -from unittest.mock import patch - -import responses -from fastapi import status -from fastapi.testclient import TestClient - -TEST_CALLBACK_URL = "https://fqdn.abc.xyz/api/resume" - - -@responses.activate -def test_playbook_endpoint_dict_inventory_success(client: TestClient, mocked_ansible_runner_run: Callable) -> None: - responses.post(url=TEST_CALLBACK_URL, status=status.HTTP_200_OK) - - params = { - "playbook_name": "placeholder.yaml", - "callback": TEST_CALLBACK_URL, - "inventory": { - "_meta": {"vars": {"host1.local": {"foo": "bar"}, "host2.local": {"hello": "world"}}}, - "all": {"hosts": {"host1.local": None, "host2.local": None}}, - }, - "extra_vars": {"dry_run": True, "commit_comment": "I am a robot!"}, - } - - with patch("lso.playbook.ansible_runner.run", new=mocked_ansible_runner_run) as _: - rv = client.post("/api/playbook/", json=params) - assert rv.status_code == status.HTTP_201_CREATED - response = rv.json() - # wait one second for the run thread to finish - time.sleep(1) - - assert isinstance(response, dict) - assert isinstance(response["job_id"], str) - responses.assert_call_count(TEST_CALLBACK_URL, 1) - - -@responses.activate -def test_playbook_endpoint_str_inventory_success(client: TestClient, mocked_ansible_runner_run: Callable) -> None: - responses.post(url=TEST_CALLBACK_URL, status=status.HTTP_200_OK) - - params = { - "playbook_name": "placeholder.yaml", - "callback": TEST_CALLBACK_URL, - "inventory": {"all": {"hosts": "host1.local\nhost2.local\nhost3.local"}}, - } - - with patch("lso.playbook.ansible_runner.run", new=mocked_ansible_runner_run) as _: - rv = client.post("/api/playbook/", json=params) - assert rv.status_code == status.HTTP_201_CREATED - response = rv.json() - # wait one second for the run thread to finish - time.sleep(1) - - assert isinstance(response, dict) - assert isinstance(response["job_id"], str) - responses.assert_call_count(TEST_CALLBACK_URL, 1) - - -@responses.activate -def test_playbook_endpoint_invalid_host_vars(client: TestClient, mocked_ansible_runner_run: Callable) -> None: - params = { - "playbook_name": "placeholder.yaml", - "callback": TEST_CALLBACK_URL, - "inventory": { - "_meta": {"host_vars": {"host1.local": {"foo": "bar"}, "host2.local": {"hello": "world"}}}, - "all": {"hosts": "host1.local\nhost2.local\nhost3.local"}, - }, - } - - with patch("lso.playbook.ansible_runner.run", new=mocked_ansible_runner_run) as _: - rv = client.post("/api/playbook/", json=params) - assert rv.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - response = rv.json() - # wait one second for the run thread to finish - time.sleep(1) - - assert isinstance(response, dict) - assert response["detail"] == [ - '[WARNING]: Skipping unexpected key (host_vars) in group (_meta), only "vars",\n', - '"children" and "hosts" are valid\n', - ] - responses.assert_call_count(TEST_CALLBACK_URL, 0) - - -@responses.activate -def test_playbook_endpoint_invalid_hosts(client: TestClient, mocked_ansible_runner_run: Callable) -> None: - params = { - "playbook_name": "placeholder.yaml", - "callback": TEST_CALLBACK_URL, - "inventory": { - "_meta": {"vars": {"host1.local": {"foo": "bar"}}}, - "all": {"hosts": ["host1.local", "host2.local", "host3.local"]}, - }, - } - - with patch("lso.playbook.ansible_runner.run", new=mocked_ansible_runner_run) as _: - rv = client.post("/api/playbook/", json=params) - assert rv.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - response = rv.json() - # wait one second for the run thread to finish - time.sleep(1) - - assert isinstance(response, dict) - assert 'Invalid "hosts" entry for "all" group' in re.sub("\n", " ", "".join(response["detail"])) - responses.assert_call_count(TEST_CALLBACK_URL, 0) diff --git a/test/test_config.py b/test/test_config.py deleted file mode 100644 index 25a41fa45279ad9e65f6ae58f8ebfcd278fe4f22..0000000000000000000000000000000000000000 --- a/test/test_config.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Set of tests that verify correct config is accepted and incorrect config is not.""" - -import json -import os -import tempfile -from pathlib import Path - -import jsonschema -import pytest - -from lso import config - - -def test_validate_testenv_config(data_config_filename: str) -> None: - """Load a configuration from a file. - - :param data_config_filename: Configuration file pytest fixture - """ - os.environ["SETTINGS_FILENAME"] = data_config_filename - params = config.load() - assert params - - -@pytest.mark.parametrize( - "bad_config", [{"name": "bad version", "version": 123}, {"name": "missing version"}, {"version": "missing name"}] -) -def test_bad_config(bad_config: dict) -> None: - with tempfile.NamedTemporaryFile(mode="w") as file: - Path(file.name).write_text(json.dumps(bad_config)) - with pytest.raises(jsonschema.ValidationError): - config.load_from_file(Path(file.name)) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 3468ba6afcc02e1acc5fa1a67ca29078a42d2fc9..0000000000000000000000000000000000000000 --- a/tox.ini +++ /dev/null @@ -1,20 +0,0 @@ -[tox] -envlist = py311 - -[testenv] -passenv = XDG_CACHE_HOME,USE_COMPOSE -setenv = - SETTINGS_FILENAME = dummy.json -deps = - coverage - -r requirements.txt - -commands = - ruff --respect-gitignore --preview . - ruff format --respect-gitignore --preview --check . - mypy . - coverage erase - coverage run --source lso -m pytest - coverage xml - coverage html - coverage report --fail-under 80