diff --git a/.gitignore b/.gitignore
index 9df71ef52ead788553d90c771678432f2fc33db0..dfdc67da9e07f8516efdc4e2f5c130db382efcff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,7 @@ docs/vale/styles/*
venv/
.venv/
+.DS_Store # macOS
+Thumbs.db # Windows
+
config.json
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e2b511ec087450ebec6817b8f894a5225bdb155d..323c1f19acd847dffd33494f6466300e576f2558 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -12,7 +12,7 @@ run-tox-pipeline:
stage: tox
tags:
- docker-executor
- image: python:3.10
+ image: python:3.11
# Change pip's cache directory to be inside the project directory since we can
# only cache local items.
diff --git a/Dockerfile b/Dockerfile
index cfbea5eed21afa529766915f91e42991e4d80fe6..8b5f4742ef0870afbb4316193ff198f2184f701d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,19 +1,31 @@
-FROM python:3.11
+FROM python:3.11-alpine
ARG ARTIFACT_VERSION
WORKDIR /app
-RUN apt update && apt install -y gcc libc-dev libffi-dev curl vim && \
+RUN apk add --update --no-cache gcc libc-dev libffi-dev curl vim ansible bash openssh && \
addgroup -S appgroup && adduser -S appuser -G appgroup -h /app
+
+# Create ansible.cfg file and set custom paths for collections and roles
+RUN mkdir -p /app/gap/collections /app/gap/roles /etc/ansible && \
+ printf "[defaults]\ncollections_paths = /app/gap/collections\nroles_path = /app/gap/roles" > /etc/ansible/ansible.cfg
+
RUN pip install \
--pre \
--extra-index-url https://artifactory.software.geant.org/artifactory/api/pypi/geant-swd-pypi/simple \
--target /app \
- goat-lso==${ARTIFACT_VERSION}
+ goat-lso==${ARTIFACT_VERSION} && \
+ ansible-galaxy collection install \
+ community.general \
+ juniper.device \
+ junipernetworks.junos \
+ geant.gap_ansible -p /app/gap/collections && \
+ ansible-galaxy role install Juniper.junos -p /app/gap/roles
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 8000
+ENTRYPOINT []
CMD ["python", "-m", "uvicorn", "lso.app:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/config.json.example b/config.json.example
index ef9197d0788b9a9576ae86f42866fffa99a7f712..64ddb818152b0fcb6bff2565084c66933b6621a4 100644
--- a/config.json.example
+++ b/config.json.example
@@ -1,3 +1,3 @@
{
- "ansible_playbooks_root_dir": "/"
+ "ansible_playbooks_root_dir": "/app/gap/collections/ansible_collections/geant/gap_ansible/playbooks"
}
diff --git a/docker/Dockerfile b/docker/Dockerfile
deleted file mode 100644
index 9fc9409ce0d4f790740f5c356c405191ab7e24fc..0000000000000000000000000000000000000000
--- a/docker/Dockerfile
+++ /dev/null
@@ -1,23 +0,0 @@
-FROM python:alpine3.17
-
-LABEL version="1.0"
-LABEL maintainer="Geant LSO Team <@geant.org>"
-
-RUN apk add --no-cache bash curl vim gcc libc-dev libffi-dev
-# RUN pip install --pre --extra-index-url https://artifactory.software.geant.org/artifactory/api/pypi/geant-swd-pypi/simple goat-lso
-
-WORKDIR /opt/lso
-COPY . .
-RUN pip install -e .
-RUN pip install httpx sphinx sphinx_rtd_theme vale ansible
-
-# Generate documentation
-RUN ./build-docs.sh
-
-# Generate sample configuration file, and remove an existing one if present
-RUN rm -f config.json >/dev/null 2>&1
-RUN ln -s config.json.example config.json
-
-# ENTRYPOINT ["sleep", "inf"]
-# ENTRYPOINT ["SETTINGS_FILENAME=./config.json", "python", "-m", "lso.app"]
-ENTRYPOINT ["./docker/app-run.sh"]
diff --git a/docker/app-run.sh b/docker/app-run.sh
deleted file mode 100755
index 6b3cd586e0455e9cb8f96d726dd3b6835a957e2b..0000000000000000000000000000000000000000
--- a/docker/app-run.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env bash
-
-SETTINGS_FILENAME=./config.json python -m lso.app
diff --git a/docker/docker-start.sh b/docker/docker-start.sh
deleted file mode 100755
index 7aba417c617cbb42b57083c693a7a55163266964..0000000000000000000000000000000000000000
--- a/docker/docker-start.sh
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/usr/bin/env bash
-
-goat_name="goat-lso"
-goat_image="goat/lso"
-goat_tag="1.0"
-
-if [[ $(docker image list | grep "${goat_image}" | grep -c "${goat_tag}") -eq 0 ]]; then
- docker build -f docker/Dockerfile -t ${goat_image}:${goat_tag} .
-fi
-if [[ $(docker ps -a | grep -c "${goat_image}:${goat_tag}") -eq 0 ]]; then
- docker run -d -p 44444:44444 --name ${goat_name} ${goat_image}:${goat_tag} >/dev/null 2>&1
-fi
-if [[ "$( docker container inspect -f '{{.State.Status}}' ${goat_name} )" != "running" ]]; then
- docker start ${goat_name} >/dev/null 2>&1
-fi
-
-sleep 1
-
-# Check endpoints
-curl -f http://localhost:44444/docs >/dev/null 2>&1
-if [[ $? -eq 0 ]]; then
- echo "LSO is running. OpenAPI available at http://localhost:44444/docs"
-else
- echo "LSO is not running"
-fi
diff --git a/docker/docker-stop.sh b/docker/docker-stop.sh
deleted file mode 100755
index 1cde4b07e467ade9ea2649ec041c08f0041deb37..0000000000000000000000000000000000000000
--- a/docker/docker-stop.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env bash
-
-docker stop goat-lso >/dev/null 2>&1
-docker rm goat-lso >/dev/null 2>&1
diff --git a/lso/config.py b/lso/config.py
index 9819b9548f925639e2b24c8a027151a0ed72dc8c..edf5546fe2020a81c43d37bbffd68ce2580d71b1 100644
--- a/lso/config.py
+++ b/lso/config.py
@@ -10,7 +10,7 @@ import os
from typing import TextIO
import jsonschema
-from pydantic import BaseModel, DirectoryPath
+from pydantic import BaseModel
CONFIG_SCHEMA = {
"$schema": "http://json-schema.org/draft-07/schema#",
@@ -25,7 +25,7 @@ DEFAULT_REQUEST_TIMEOUT = 10
class Config(BaseModel):
"""Simple Config class that only contains the path to the used Ansible playbooks."""
- ansible_playbooks_root_dir: DirectoryPath
+ ansible_playbooks_root_dir: str
def load_from_file(file: TextIO) -> Config:
diff --git a/lso/playbook.py b/lso/playbook.py
index 19ff44e72402499ed111841aa921421e08d006bd..9650985340496fd7e166e8e9dea740f8b2ba6bbb 100644
--- a/lso/playbook.py
+++ b/lso/playbook.py
@@ -18,8 +18,6 @@ from lso.config import DEFAULT_REQUEST_TIMEOUT
logger = logging.getLogger(__name__)
-config_params = config.load()
-
# enum.StrEnum is only available in python 3.11
class PlaybookJobStatus(str, enum.Enum):
@@ -50,6 +48,7 @@ class PlaybookLaunchResponse(BaseModel):
def get_playbook_path(playbook_name: str) -> str:
+ config_params = config.load()
return os.path.join(config_params.ansible_playbooks_root_dir, playbook_name)
diff --git a/test/conftest.py b/test/conftest.py
index 78112127ae71436ed6b44fae8a03c290af92c1bf..2e3586910cb413d536185eb6e0dde86a70186157 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -1,7 +1,8 @@
import json
import os
import tempfile
-from typing import Any, Generator
+from io import StringIO
+from typing import Any, Callable, Generator
import pytest
from faker import Faker
@@ -10,28 +11,42 @@ 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:
+ return Runner()
+
+ return run
+
+
@pytest.fixture(scope="session")
-def config_data() -> dict[str, str]:
+def configuration_data() -> dict[str, str]:
"""Start the server with valid configuration data."""
- return {"ansible_playbooks_root_dir": "/"}
+ return {"ansible_playbooks_root_dir": "/app/gap/collections/ansible_collections/geant/gap_ansible/playbooks"}
@pytest.fixture(scope="session")
-def config_file(config_data: dict[str, str]) -> Generator[str, Any, None]:
+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(config_data))
+ file.write(json.dumps(configuration_data))
file.flush()
yield file.name
@pytest.fixture(scope="session")
-def client(config_file: str) -> Generator[TestClient, Any, None]:
+def client(data_config_filename: str) -> Generator[TestClient, Any, None]:
"""Return a client that can be used to test the server."""
- os.environ["SETTINGS_FILENAME"] = config_file
+ os.environ["SETTINGS_FILENAME"] = data_config_filename
app = lso.create_app()
yield TestClient(app) # wait here until calling context ends
diff --git a/test/routes/__init__.py b/test/routes/__init__.py
index b1490578a4f0cb8871454268461cb9c9b582878b..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/test/routes/__init__.py
+++ b/test/routes/__init__.py
@@ -1,15 +0,0 @@
-from io import StringIO
-from typing import Any
-
-TEST_CALLBACK_URL = "https://fqdn.abc.xyz/api/resume"
-
-
-class Runner:
- def __init__(self) -> None:
- self.status = "success"
- self.rc = 0
- self.stdout = StringIO("[{'step one': 'results'}, {'step two': 2}]")
-
-
-def test_ansible_runner_run(**kwargs: Any) -> Runner:
- return Runner()
diff --git a/test/routes/test_ip_trunk.py b/test/routes/test_ip_trunk.py
index 99a8d9afd41b90a7479c2e7fd9521902fe25d7c4..21f74cae880a5aea2b40aedd381a4b4281094a83 100644
--- a/test/routes/test_ip_trunk.py
+++ b/test/routes/test_ip_trunk.py
@@ -1,4 +1,5 @@
import time
+from typing import Callable
from unittest.mock import patch
import jsonschema
@@ -8,7 +9,8 @@ from faker import Faker
from starlette.testclient import TestClient
from lso.playbook import PlaybookLaunchResponse
-from test.routes import TEST_CALLBACK_URL, test_ansible_runner_run
+
+TEST_CALLBACK_URL = "https://fqdn.abc.xyz/api/resume"
@pytest.fixture(scope="session")
@@ -157,7 +159,9 @@ def migration_object(faker: Faker) -> dict:
@responses.activate
-def test_ip_trunk_provisioning(client: TestClient, subscription_object: dict) -> None:
+def test_ip_trunk_provisioning(
+ client: TestClient, subscription_object: dict, mocked_ansible_runner_run: Callable
+) -> None:
responses.post(url=TEST_CALLBACK_URL, status=200)
params = {
@@ -170,7 +174,7 @@ def test_ip_trunk_provisioning(client: TestClient, subscription_object: dict) ->
"subscription": subscription_object,
}
- with patch("lso.playbook.ansible_runner.run", new=test_ansible_runner_run) as _:
+ with patch("lso.playbook.ansible_runner.run", new=mocked_ansible_runner_run) as _:
rv = client.post("/api/ip_trunk/", json=params)
assert rv.status_code == 200
response = rv.json()
@@ -184,7 +188,9 @@ def test_ip_trunk_provisioning(client: TestClient, subscription_object: dict) ->
@responses.activate
-def test_ip_trunk_modification(client: TestClient, subscription_object: dict) -> None:
+def test_ip_trunk_modification(
+ client: TestClient, subscription_object: dict, mocked_ansible_runner_run: Callable
+) -> None:
responses.post(url=TEST_CALLBACK_URL, status=200)
params = {
@@ -197,7 +203,7 @@ def test_ip_trunk_modification(client: TestClient, subscription_object: dict) ->
"old_subscription": subscription_object,
}
- with patch("lso.playbook.ansible_runner.run", new=test_ansible_runner_run) as _:
+ with patch("lso.playbook.ansible_runner.run", new=mocked_ansible_runner_run) as _:
rv = client.put("/api/ip_trunk/", json=params)
assert rv.status_code == 200
response = rv.json()
@@ -211,7 +217,7 @@ def test_ip_trunk_modification(client: TestClient, subscription_object: dict) ->
@responses.activate
-def test_ip_trunk_deletion(client: TestClient, subscription_object: dict) -> None:
+def test_ip_trunk_deletion(client: TestClient, subscription_object: dict, mocked_ansible_runner_run: Callable) -> None:
responses.post(url=TEST_CALLBACK_URL, status=204)
params = {
@@ -223,7 +229,7 @@ def test_ip_trunk_deletion(client: TestClient, subscription_object: dict) -> Non
"subscription": subscription_object,
}
- with patch("lso.playbook.ansible_runner.run", new=test_ansible_runner_run) as _:
+ with patch("lso.playbook.ansible_runner.run", new=mocked_ansible_runner_run) as _:
rv = client.request(url="/api/ip_trunk/", method=responses.DELETE, json=params)
assert rv.status_code == 200
response = rv.json()
@@ -237,7 +243,9 @@ def test_ip_trunk_deletion(client: TestClient, subscription_object: dict) -> Non
@responses.activate
-def test_ip_trunk_migration(client: TestClient, subscription_object: dict, migration_object: dict) -> None:
+def test_ip_trunk_migration(
+ client: TestClient, subscription_object: dict, migration_object: dict, mocked_ansible_runner_run: Callable
+) -> None:
responses.post(url=TEST_CALLBACK_URL, status=204)
params = {
@@ -251,7 +259,7 @@ def test_ip_trunk_migration(client: TestClient, subscription_object: dict, migra
"new_side": migration_object,
}
- with patch("lso.playbook.ansible_runner.run", new=test_ansible_runner_run) as _:
+ with patch("lso.playbook.ansible_runner.run", new=mocked_ansible_runner_run) as _:
rv = client.post(url="/api/ip_trunk/migrate", json=params)
assert rv.status_code == 200
response = rv.json()
diff --git a/test/routes/test_router.py b/test/routes/test_router.py
index 1b8a7bde2b279749c24a20d5810f506b03a74056..6a9f2fffa21e847a1e3294f1658ce312a6fe3a10 100644
--- a/test/routes/test_router.py
+++ b/test/routes/test_router.py
@@ -1,4 +1,5 @@
import time
+from typing import Callable
from unittest.mock import patch
import jsonschema
@@ -7,11 +8,12 @@ from faker import Faker
from starlette.testclient import TestClient
from lso.playbook import PlaybookLaunchResponse
-from test.routes import TEST_CALLBACK_URL, test_ansible_runner_run
+
+TEST_CALLBACK_URL = "https://fqdn.abc.xyz/api/resume"
@responses.activate
-def test_router_provisioning(client: TestClient, faker: Faker) -> None:
+def test_router_provisioning(client: TestClient, faker: Faker, mocked_ansible_runner_run: Callable) -> None:
responses.put(url=TEST_CALLBACK_URL, status=200)
params = {
@@ -40,7 +42,7 @@ def test_router_provisioning(client: TestClient, faker: Faker) -> None:
},
}
- with patch("lso.playbook.ansible_runner.run", new=test_ansible_runner_run) as _:
+ with patch("lso.playbook.ansible_runner.run", new=mocked_ansible_runner_run) as _:
rv = client.post("/api/router/", json=params)
assert rv.status_code == 200
response = rv.json()
diff --git a/test/test_config.py b/test/test_config.py
index 83a47d172bc486495a85900a56a25c73941020e7..bb724c7f74638f079491d895675674676454b6e2 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -9,12 +9,12 @@ import pytest
from lso import config
-def test_validate_testenv_config(config_file: str) -> None:
+def test_validate_testenv_config(data_config_filename: str) -> None:
"""Load a configuration from a file.
- :param config_file: Configuration file pytest fixture
+ :param data_config_filename: Configuration file pytest fixture
"""
- os.environ["SETTINGS_FILENAME"] = config_file
+ os.environ["SETTINGS_FILENAME"] = data_config_filename
params = config.load()
assert params
diff --git a/tox.ini b/tox.ini
index e8715b0ffb439b3ec55e7e9eb56f087b46e21aad..927b8974a48b33a3031b4b28dce6c342837cf1eb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py310
+envlist = py311
[flake8]
ignore = W503
@@ -15,19 +15,19 @@ per-file-ignores =
[testenv]
passenv = XDG_CACHE_HOME,USE_COMPOSE
setenv =
- SETTINGS_FILENAME = config.json.example
+ SETTINGS_FILENAME = dummy.json
deps =
coverage
-r requirements.txt
commands =
- coverage erase
- coverage run --source lso -m pytest
- coverage xml
- coverage html
- coverage report --fail-under 80
isort -c .
ruff .
black --check .
mypy .
flake8
+ coverage erase
+ coverage run --source lso -m pytest
+ coverage xml
+ coverage html
+ coverage report --fail-under 80