From 1c700deb66270456ef0263b39754ed0b7b3e35de Mon Sep 17 00:00:00 2001 From: "Diaz de Arcaya Serrano, Josu" <josu.diazdearcaya@tecnalia.com> Date: Thu, 18 Jan 2024 11:24:52 +0100 Subject: [PATCH] updating main branch --- README.md | 32 +- docs/docs_files/01-intro.rst | 7 +- docs/kr-10.feature | 49 +++ docs/sequence-diagrams/.gitignore | 1 + .../51-request-deployment-status.puml | 26 ++ .../51-start-deployment.puml | 34 ++ .../51-start-undeployment.puml | 34 ++ docs/sequence-diagrams/README.md | 27 ++ iem-api/.python-version | 2 +- iem-api/Dockerfile | 25 +- iem-api/certs/config | 3 + iem-api/certs/id_rsa | 27 ++ iem-api/certs/id_rsa.pub | 1 + iem-api/db/.gitignore | 1 + iem-api/logging.ini | 31 ++ iem-api/main.py | 110 ++++-- iem-api/requirements.txt | 3 +- iem-api/src/__init__.py | 1 + iem-api/src/_version.py | 4 + iem-api/src/core/engine.py | 139 ++++--- iem-api/src/core/iem.py | 342 +++++++++++++----- iem-api/src/core/persistence.py | 86 +---- iem-api/src/core/utils.py | 39 +- iem-api/src/resources/id_iem | 38 ++ iem-api/src/resources/id_iem.pub | 1 + iem-api/tests/__init__.py | 0 iem-api/tests/it/__init__.py | 0 iem-api/tests/it/test_it_iem.py | 149 ++++++++ iem-api/tests/resources/ansible.zip | Bin 0 -> 3735 bytes iem-api/tests/resources/aws.zip | Bin 0 -> 1230 bytes iem-api/tests/resources/docker.zip | Bin 0 -> 5400 bytes iem-api/tests/resources/dummy.zip | Bin 0 -> 3700 bytes iem-api/tests/resources/main.yml | 8 + iem-api/tests/resources/openstack.zip | Bin 0 -> 2117 bytes iem-api/tests/resources/shs-bundle.zip | Bin 0 -> 2223 bytes iem-api/tests/resources/shs.zip | Bin 0 -> 2169 bytes iem-api/tests/unit/__init__.py | 0 iem-api/tests/unit/test_iem.py | 31 ++ iem-api/tests/unit/test_main.py | 224 ++++++++++++ iem-api/tests/unit/test_persistence.py | 32 ++ openapi.json | 2 +- sonar-project.properties | 5 + 42 files changed, 1195 insertions(+), 319 deletions(-) create mode 100644 docs/kr-10.feature create mode 100755 docs/sequence-diagrams/.gitignore create mode 100644 docs/sequence-diagrams/51-request-deployment-status.puml create mode 100644 docs/sequence-diagrams/51-start-deployment.puml create mode 100644 docs/sequence-diagrams/51-start-undeployment.puml create mode 100755 docs/sequence-diagrams/README.md create mode 100644 iem-api/certs/config create mode 100644 iem-api/certs/id_rsa create mode 100644 iem-api/certs/id_rsa.pub create mode 100644 iem-api/db/.gitignore create mode 100644 iem-api/logging.ini create mode 100644 iem-api/src/__init__.py create mode 100644 iem-api/src/_version.py create mode 100644 iem-api/src/resources/id_iem create mode 100644 iem-api/src/resources/id_iem.pub create mode 100644 iem-api/tests/__init__.py create mode 100644 iem-api/tests/it/__init__.py create mode 100644 iem-api/tests/it/test_it_iem.py create mode 100644 iem-api/tests/resources/ansible.zip create mode 100644 iem-api/tests/resources/aws.zip create mode 100644 iem-api/tests/resources/docker.zip create mode 100644 iem-api/tests/resources/dummy.zip create mode 100644 iem-api/tests/resources/main.yml create mode 100644 iem-api/tests/resources/openstack.zip create mode 100644 iem-api/tests/resources/shs-bundle.zip create mode 100644 iem-api/tests/resources/shs.zip create mode 100644 iem-api/tests/unit/__init__.py create mode 100644 iem-api/tests/unit/test_iem.py create mode 100644 iem-api/tests/unit/test_main.py create mode 100644 iem-api/tests/unit/test_persistence.py create mode 100644 sonar-project.properties diff --git a/README.md b/README.md index b18ac48..4daa1ef 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,27 @@ # T51 IaC Executor Manager -Running the server +Running the server with uvicorn + ```bash uvicorn main:app --reload ``` -###### Containers +Execute it directly -Containerize the IEM ```bash -docker build --build-arg API_KEY=$API_KEY -t optima-piacere-docker-dev.artifact.tecnalia.com/wp5/iem-api:y1 . +./main.py ``` -Similarly, docker compose can be used to build both -```bash -docker-compose build -``` +###### Containers + +Containerize the IEM -It can also be used to push them to the registry ```bash -docker-compose push +docker build --build-arg API_KEY=$API_KEY -t optima-piacere-docker-dev.artifact.tecnalia.com/wp5/iem-api:y1 . ``` -Run the IEM +Run the dockerized IEM + ```bash docker run -p 8000:8000 optima-piacere-docker-dev.artifact.tecnalia.com/wp5/iem-api:y1 ``` @@ -41,4 +40,15 @@ Run a single test nose2 -v tests.core.test_iem.TestIem.test_deploy_destroy_openstack ``` +Run unit and integration tests + +```bash +nose2 -v tests.unit +nose2 -v tests.it +``` + +Integration tests are prevented from being executed unless we deliberately define an environment variable +```bash +AWS=1 nose2 -v tests.it +``` diff --git a/docs/docs_files/01-intro.rst b/docs/docs_files/01-intro.rst index d3563bc..45533a1 100644 --- a/docs/docs_files/01-intro.rst +++ b/docs/docs_files/01-intro.rst @@ -4,6 +4,7 @@ Introduction ************ -The IaC Execution Manager utilizes different technologies that can be used for the provisioning, configuration, and orchestration of the different infrastructural devices that can be found in a production deployment. This has served us to provide evidence and reasoning for the selection of the technologies that the IEM prototype is going to utilize. - -This prototype is viable for the deployment of different IaC technologies that cover the provisioning and the configuration of the infrastructural devices required for the projects utilizing the PIACERE framework. It provides a unified interface for other components so they can interact with the IEM in a unified manner. It can also be deployed in production utilizing container-based technologies which makes this prototype viable to be operationalized in public and private cloud provides, and on premises. For this prototype, the IEM supports two well established technologies (i.e. Ansible and Terraform) that are able to provision the different infrastructural devices required by the use cases, and the configuration of each of these infrastructural devices so they can accommodate the applications to be allocated. +.. + TODO Provide a brief description of the component here. Outline its goals, functionalities, etc.; + Mention subcomponents or extra delivered tools etc., with rst references to adequate sections. + \ No newline at end of file diff --git a/docs/kr-10.feature b/docs/kr-10.feature new file mode 100644 index 0000000..2dbbdf9 --- /dev/null +++ b/docs/kr-10.feature @@ -0,0 +1,49 @@ +Feature: PIACERE Run Time + +# The input of this scenario is detailed in the following +# https://git.code.tecnalia.com/piacere/private/t51-iem/iem/-/blob/y2/openapi.json#/deployments/deploy_deployments__post +# The following scenario relates to REQ81, REQ83, REQ84, REQ87 +Scenario: Deploy a fresh project which comprises terraform, ansible, and docker +Given a project bundle in the relevant IaC technologies (terraform, ansible, docker-compose), the deployment id, and the required cloud credentials + When the user triggers the deployment + Then the IEM is invoked + And executes the stages of the bundle asyncronously + And the user is notified that the deployment has been accepted + +# The input of this scenario is detailed in the following +# https://git.code.tecnalia.com/piacere/private/t51-iem/iem/-/blob/y2/openapi.json#/deployments/read_status_deployment_deployments__deployment_id__get +# The following scenario relates to REQ55, REQ82 +Scenario: Query the status of a running project +Given the deployment id of an already existing project + When the user queries the status of the project + Then the IEM is invoked + And the user is notified of the status + +# The input of this scenario is detailed in the following +# https://git.code.tecnalia.com/piacere/private/t51-iem/iem/-/blob/y2/openapi.json#/deployments/undeploy_undeploy__post +# The following scenario relates to REQ81, REQ83, REQ84, REQ85 +Scenario: Undeploy a project +Given the deployment id of an already existing project and the required cloud credentials + When the user triggers the undeployment + Then the IEM is invoked + And tears down the entire deployment asyncronously + And the user is notified that the undeployment has been accepted + +# The input of this scenario is detailed in the following +# https://git.code.tecnalia.com/piacere/private/t51-iem/iem/-/blob/y2/openapi.json#/deployments/read_status_deployment_deployments__deployment_id__get +# The following scenario relates to REQ55, REQ82 +Scenario: Query the status of an undeployed project +Given the deployment id of an undeployed project + When the user queries the status of the project + Then the IEM is invoked + And the user is notified of the status + +# The input of this scenario is detailed in the following +# https://git.code.tecnalia.com/piacere/private/t51-iem/iem/-/blob/y2/openapi.json#/deployments/deploy_deployments__post +# The following scenario relates to REQ12, REQ81, REQ83, REQ84, REQ87 +Scenario: Redeploy a project +Given a project bundle in the relevant IaC technologies (terraform, ansible, docker-compose), the deployment id, and the required cloud credentials + When the user triggers the deployment + Then the IEM is invoked + And executes the stages of the bundle asyncronously + And the user is notified that the deployment has been accepted diff --git a/docs/sequence-diagrams/.gitignore b/docs/sequence-diagrams/.gitignore new file mode 100755 index 0000000..981aeb8 --- /dev/null +++ b/docs/sequence-diagrams/.gitignore @@ -0,0 +1 @@ +/out \ No newline at end of file diff --git a/docs/sequence-diagrams/51-request-deployment-status.puml b/docs/sequence-diagrams/51-request-deployment-status.puml new file mode 100644 index 0000000..752fd95 --- /dev/null +++ b/docs/sequence-diagrams/51-request-deployment-status.puml @@ -0,0 +1,26 @@ +@startuml + +title Request the Current Status of a Deployment + +participant "PRC" as DESIDE + +box "IaC Execution Manager" #LightBlue +participant "Rest API" as RTIEM_api #99FF99 +participant Core as RTIEM_core #99FF99 +participant Persistence as RTIEM_db #99FF99 +end box + + +DESIDE -> RTIEM_api: Deployment Status Request + +RTIEM_api -> RTIEM_core: Deployment Status Request + +RTIEM_core -> RTIEM_db: Deployment Status Request + +RTIEM_core <-- RTIEM_db: Deployment Status Response + +RTIEM_api <-- RTIEM_core: Deployment Status Response + +DESIDE <-- RTIEM_api: Deployment Status Response + +@enduml diff --git a/docs/sequence-diagrams/51-start-deployment.puml b/docs/sequence-diagrams/51-start-deployment.puml new file mode 100644 index 0000000..ef8b0ba --- /dev/null +++ b/docs/sequence-diagrams/51-start-deployment.puml @@ -0,0 +1,34 @@ +@startuml + +title Initiate Deployment + +participant "Runtime Controller (PRC)" as RTPRC + +box "IaC Execution Manager" #LightBlue +participant "Rest API" as RTIEM_api #99FF99 +participant Core as RTIEM_core #99FF99 +participant Persistence as RTIEM_db #99FF99 +participant "Executor" as executor #99FF99 + +end box + +collections "Resource Provider" as infraresource + +RTPRC -> RTIEM_api: Deployment Request +RTPRC <-- RTIEM_api: Deployment Response + +RTIEM_api -> RTIEM_core: Deployment Request + +RTIEM_core -> RTIEM_db: Save Deployment Started + +RTIEM_core -> executor: Deployment Request + +executor -> infraresource: Deploy Commands +executor -> infraresource: ... +executor -> infraresource: Deploy Commands + +executor -> RTIEM_core: Deployment Response + +RTIEM_core -> RTIEM_db: Save Deployment Status + +@enduml diff --git a/docs/sequence-diagrams/51-start-undeployment.puml b/docs/sequence-diagrams/51-start-undeployment.puml new file mode 100644 index 0000000..35bc219 --- /dev/null +++ b/docs/sequence-diagrams/51-start-undeployment.puml @@ -0,0 +1,34 @@ +@startuml + +title Initiate Undeployment + +participant "Runtime Controller (PRC)" as RTPRC + +box "IaC Execution Manager" #LightBlue +participant "Rest API" as RTIEM_api #99FF99 +participant Core as RTIEM_core #99FF99 +participant Persistence as RTIEM_db #99FF99 +participant "Executor" as executor #99FF99 +end box + +collections "Resource Provider" as infraresource + +RTPRC -> RTIEM_api: Undeployment Request +RTPRC <-- RTIEM_api: Undeployment Response + +RTIEM_api -> RTIEM_core: Undeployment Request + +RTIEM_core -> RTIEM_db: Save Undeployment Started + +RTIEM_core -> executor: Undeployment Request + +executor -> infraresource: Uneploy Commands +executor -> infraresource: ... +executor -> infraresource: Undeploy Commands + +executor -> RTIEM_core: Undeployment Response + +RTIEM_core -> RTIEM_db: Save Undeployment Status + +@enduml + diff --git a/docs/sequence-diagrams/README.md b/docs/sequence-diagrams/README.md new file mode 100755 index 0000000..cc0a33c --- /dev/null +++ b/docs/sequence-diagrams/README.md @@ -0,0 +1,27 @@ +# T51 IaC Executor Manager Secuence diagrams + +This folder contains the sequence diagrams developed for the T51 IEM. They have been developed using plantuml +* https://plantuml.com + +These files follow a very simple text based syntax. ie +``` +Bob->Alice : Hello! +``` +which renders (providing plantuml is enabled in gitlab https://docs.gitlab.com/ee/administration/integration/plantuml.html) as + +```plantuml +Bob->Alice : Hello! +``` +we can also specify a file + +```plantuml source="51-start-deployment.puml" +``` + +To be able to edit them and check the rendering there are several options: +* Edit and generate the file using the jar, which is not very user friendly +``` java -jar plantuml.jar sequenceDiagram.txt ``` +* Use an IDE and a plugin. There are plugins available for different IDEs,i.e. + * eclipse https://plantuml.com/eclipse + * visual code https://marketplace.visualstudio.com/items?itemName=jebbs.plantuml + + diff --git a/iem-api/.python-version b/iem-api/.python-version index f69abe4..0a59033 100644 --- a/iem-api/.python-version +++ b/iem-api/.python-version @@ -1 +1 @@ -3.9.7 +3.9.10 diff --git a/iem-api/Dockerfile b/iem-api/Dockerfile index 635043b..3010302 100644 --- a/iem-api/Dockerfile +++ b/iem-api/Dockerfile @@ -1,17 +1,16 @@ FROM hashicorp/terraform:1.1.4 -ARG API_KEY +COPY requirements.txt /tmp/requirements.txt +RUN apk add py3-pip cargo g++ python3-dev file libffi-dev openssl-dev bash python3 gnupg +RUN pip3 install -r /tmp/requirements.txt +# install docker stack +RUN apk add docker docker-compose -ENV API_KEY=$API_KEY +ENV API_KEY=changeme ENV IEM_HOME=/opt/iem/ +ENV DOCKERIZED=true COPY src/resources/ansible.cfg /etc/ansible/ansible.cfg -COPY requirements.txt /tmp/requirements.txt -COPY src ${IEM_HOME}src -COPY main.py ${IEM_HOME}main.py - -RUN apk add py3-pip cargo g++ python3-dev file libffi-dev openssl-dev bash python3=3.9.13-r1 gnupg -RUN pip3 install -r /tmp/requirements.txt # RUN adduser -h ${IEM_HOME} -S -D iem COPY certs/config ${IEM_HOME}.ssh/config @@ -24,11 +23,17 @@ RUN adduser -h ${IEM_HOME} -S -D iem && \ chmod 0600 ${IEM_HOME}.ssh/id_rsa && \ chmod 0644 ${IEM_HOME}.ssh/id_rsa.pub USER iem -RUN ansible-galaxy collection install community.general COPY roles.yml /tmp/roles.yml RUN ansible-galaxy install -r /tmp/roles.yml +RUN mkdir -p ${IEM_HOME}db && \ + mkdir -p ${IEM_HOME}deployments + +COPY src ${IEM_HOME}src +COPY main.py ${IEM_HOME}main.py +COPY logging.ini ${IEM_HOME}logging.ini + ENTRYPOINT ["/usr/bin/env"] WORKDIR ${IEM_HOME} -CMD /usr/bin/uvicorn main:app --host 0.0.0.0 +CMD /usr/bin/uvicorn main:app --host 0.0.0.0 --log-level info EXPOSE 8000 diff --git a/iem-api/certs/config b/iem-api/certs/config new file mode 100644 index 0000000..a3dd79d --- /dev/null +++ b/iem-api/certs/config @@ -0,0 +1,3 @@ +Host * + StrictHostKeyChecking no + UserKnownHostsFile=/dev/null \ No newline at end of file diff --git a/iem-api/certs/id_rsa b/iem-api/certs/id_rsa new file mode 100644 index 0000000..86a197b --- /dev/null +++ b/iem-api/certs/id_rsa @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA1FrTNE42EgZr9WJNMtvpKFHYhPUJ4lzEp83EM0jYY3TyjmIe +ThMuqMLAHCk22fl4a8PttucggJ5ZWKhcJh623/y8AybJcmqZgq9a41Q609dmirf0 +7frCl+6zL8Mqy2Le2BD4eRADcq11s8r8Ys6J+EBPHQgEnK9CeZLSc/WFRlVr4bOD +s0bEouDxjTAMYjYcpsCwqYgGdIXI9WWsnt3RvcEe8CaiTqoyDN8ZtgkG6MweSrTQ +js8ySHO6o25cOoF7aT9Ihhf32I+KUanNIOvk3RAw2z1FK5xkFbbqMggZqz7rJn3M +sn2dDiCQi2CWox2OYXV/jJKLC3UFuOX64fS9cwIDAQABAoIBAQCs69Tm1/Vx0ibh +aA4DJ06C1bsh8cP9v5soJgfp1xzWSGooBcA1xasOI6B6jhkrgNlNr/uIIEe4VLne +1yJKrGIwnUagrloGQMYGxDKXwYQx80p+FXRuwe7p96eUcjIL8tQSUCd1tdOI87VQ +FjBVaWiybfO+aUQQLytLgoK7iKfhb7vO+9F+ZK1iDjBDNxFuiOM5zoeWOI7boYkD +2yXIkwoBePS2rosbPLa649sVakKex2WhQdUFst4Zba2RhnWQBXUY44LvEK5TzScF +FyYphPOUSplbzzM2+fuOna91NIWmJyHmf15lj7X9kC66XFIZMlvapksB8stEpDiA +4al3IdBJAoGBAPPuM3xkr/kQYYn7E42fgpmINZ78V48gMLhpyUOabZtF8inMyMPB +q7kfHns8Vx0ET8grSNr3qwDDV82lwvGqRCFChASMdQRR9LanydkDSeqpuZyQtVlt +A/65YUdcNY7Vy+M+fRh5Srh/6qcO3beLeLWXbJ4RHBP/OEmHuF4mLfgVAoGBAN7c +qdxTOhXPvOU69Bs5rJdfo6qBI1Yg8MCGctsUwPFpw1kW773ROOPa6XI6D74Dsdg8 +ypZ+IC3pRVtx61Xi3NOwxWNTTG+dyUgTSFz+WKjywXZXeHIbWngiFqk8JFYQWPzk +6YaJk4tZhk2YuNNaCCYRgQqyWv8coEurRlMXZHlnAoGBALcJwdaQ0z8oXJimL4jw +7ZX5kIrpPWanuAdZUe4Jfj+qX8mf4fKKbCowQLYmlBOw/ZDtcfDlMYsUCdnFjZ+7 +rP3sJJYpM1F3khJRm3PdNOUCUMY8C+i7lejZADcE6SdyJFkztbjcowYI7nJHBHZL +ENvqcVW27wPOWlVKozz6lzn1AoGALVwmaoS6DtRwcwuzwZLUkR7TNhIAujgMKHN1 +DyhDOR+4tfpYI39hH+dfmnM83wTrfsKozUawkAepqToflySMo72X/2Zl6VXpMPVT +xjGyo/h87fRRvI/asxblG9702luLcTW6XjrEQBmhn0uVWtc5T15CsIWqxb/y1FPx +BVp+hcMCgYAlJXbjzjbbDoIOCsXPSPe9voBL8zVwp0aNuvQcuB/vCt1n1c1DWuPr +AGMy/fRwY0Znag+ODMuulm7RgXUQy6ifJHiz9cKVGg/mGifaJSjgC+1AI9HFlij3 +asM5CueU0gK974rDxQkwmIWpRH57+kf6s8tGDrPPvqX9S4p3oxFlTw== +-----END RSA PRIVATE KEY----- diff --git a/iem-api/certs/id_rsa.pub b/iem-api/certs/id_rsa.pub new file mode 100644 index 0000000..245caaf --- /dev/null +++ b/iem-api/certs/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUWtM0TjYSBmv1Yk0y2+koUdiE9QniXMSnzcQzSNhjdPKOYh5OEy6owsAcKTbZ+Xhrw+225yCAnllYqFwmHrbf/LwDJslyapmCr1rjVDrT12aKt/Tt+sKX7rMvwyrLYt7YEPh5EANyrXWzyvxizon4QE8dCAScr0J5ktJz9YVGVWvhs4OzRsSi4PGNMAxiNhymwLCpiAZ0hcj1Zaye3dG9wR7wJqJOqjIM3xm2CQbozB5KtNCOzzJIc7qjblw6gXtpP0iGF/fYj4pRqc0g6+TdEDDbPUUrnGQVtuoyCBmrPusmfcyyfZ0OIJCLYJajHY5hdX+MkosLdQW45frh9L1z josu@WKM0092A diff --git a/iem-api/db/.gitignore b/iem-api/db/.gitignore new file mode 100644 index 0000000..8d96d78 --- /dev/null +++ b/iem-api/db/.gitignore @@ -0,0 +1 @@ +iem.db diff --git a/iem-api/logging.ini b/iem-api/logging.ini new file mode 100644 index 0000000..7a1ca11 --- /dev/null +++ b/iem-api/logging.ini @@ -0,0 +1,31 @@ +[loggers] +keys=root,src,uvicorn + +[handlers] +keys=stream_handler + +[formatters] +keys=formatter + +[logger_root] +level=INFO +handlers=stream_handler + +[logger_src] +level=INFO +handlers=stream_handler +qualname=src +propagate=0 + +[logger_uvicorn] +level=INFO +handlers=stream_handler +qualname=uvicorn +propagate=0 + +[handler_stream_handler] +class=StreamHandler +formatter=formatter + +[formatter_formatter] +format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s diff --git a/iem-api/main.py b/iem-api/main.py index 3fb84f4..ec8ad09 100755 --- a/iem-api/main.py +++ b/iem-api/main.py @@ -1,33 +1,34 @@ +#!/usr/bin/env python3 + import json import logging -import os - -from fastapi import FastAPI, BackgroundTasks, status, Security, Depends, HTTPException -from fastapi.openapi.utils import get_openapi -from fastapi.security.api_key import APIKeyHeader, APIKey from typing import List +import uvicorn +from fastapi import (BackgroundTasks, Depends, FastAPI, HTTPException, + Security, status) +from fastapi.openapi.utils import get_openapi +from fastapi.security.api_key import APIKey, APIKeyHeader +from src import buildno, major, minor, revision from src.core.iem import Iem -from src.core.persistence import Sqlite -from src.core.utils import ( - BaseResponse, - DeploymentResponse, - DeploymentRequest, - DeleteDeploymentRequest, -) - -LOGGER = logging.getLogger("iem") +from src.core.persistence import Persistence +from src.core.utils import (BaseResponse, DeleteDeploymentRequest, + DeploymentRequest, DeploymentResponse, + SelfHealingRequest, DeploymentStatusRequest) api_key_header = APIKeyHeader(name="x-api-key", auto_error=False) app = FastAPI( - title="IaC Execution Manager", version="0.1.15", description="IaC Execution Manager" + title="IaC Execution Manager", + version=f"{major}.{minor}.{revision}.{buildno}", + description="IaC Execution Manager", ) +logging.config.fileConfig("logging.ini") -async def get_api_key(api_key_query: str = Security(api_key_header)): - if Sqlite().valid_api_key(api_key_query=api_key_query): +async def get_api_key(api_key_query: str = Security(api_key_header)): + if Persistence().valid_api_key(api_key_query=api_key_query): return api_key_query else: raise HTTPException( @@ -37,22 +38,18 @@ async def get_api_key(api_key_query: str = Security(api_key_header)): @app.get("/", tags=["greeting"]) -async def read_root(api_key: APIKey = Depends(get_api_key)): +async def read_root(_: APIKey = Depends(get_api_key)): return { "message": "Hello from the IaC Execution Manager!", "version": app.version, "terraform": "1.1.4", - "ansible": "5.5.0", + "ansible": "8.5.0", } @app.get("/deployments/", response_model=List[DeploymentResponse], tags=["deployments"]) async def read_status( - start: int = 0, - count: int = 25, - start_date: str = "1970-01-01", - end_date: str = "2100-01-01", - api_key: APIKey = Depends(get_api_key), + _: APIKey = Depends(get_api_key), ): all_deployments = Iem(credentials=None).get_all_deployments() return list(all_deployments) @@ -65,14 +62,29 @@ async def read_status( ) async def read_status_deployment( deployment_id: str, - start: int = 0, - count: int = 1, - api_key: APIKey = Depends(get_api_key), + _: APIKey = Depends(get_api_key), ): deployment = Iem().get_deployment(deployment_id=deployment_id) return list(deployment) +@app.get( + "/deployments/{deployment_id}/stages/{stage_id}/outputs", + response_model=dict, + tags=["deployments"], +) +async def read_deployment_outputs( + deployment_id: str, + stage_id: str, + d: DeploymentStatusRequest, + _: APIKey = Depends(get_api_key), +): + outputs = Iem(credentials=d.credentials).get_deployment_outputs( + deployment_id=deployment_id, stage_id=stage_id + ) + return outputs + + @app.post( "/deployments/", status_code=status.HTTP_201_CREATED, @@ -82,11 +94,10 @@ async def read_status_deployment( async def deploy( d: DeploymentRequest, background_tasks: BackgroundTasks, - api_key: APIKey = Depends(get_api_key), + _: APIKey = Depends(get_api_key), ): - logging.warning(d) i = Iem(credentials=d.credentials) - background_tasks.add_task(i.deploy, d.deployment_id, d.repository, d.commit) + background_tasks.add_task(i.deploy, d.deployment_id, d.bundle.base64) return BaseResponse(message="Deployment Request Created") @@ -99,15 +110,48 @@ async def deploy( async def undeploy( d: DeleteDeploymentRequest, background_tasks: BackgroundTasks, - api_key: APIKey = Depends(get_api_key), + _: APIKey = Depends(get_api_key), ): - logging.warning(d) i = Iem(credentials=d.credentials) background_tasks.add_task(i.destroy, d.deployment_id) return BaseResponse(message="Undeployment Request Created") -if os.getenv("STAGE") == "dev": +@app.post( + "/deployments/{deployment_id}/self-healing", + status_code=status.HTTP_201_CREATED, + response_model=BaseResponse, + tags=["deployments"], +) +async def self_healing_strategy( + deployment_id: str, + d: SelfHealingRequest, + background_tasks: BackgroundTasks, + _: APIKey = Depends(get_api_key), +): + i = Iem(credentials=d.credentials) + background_tasks.add_task(i.self_healing_strategy, deployment_id, d.playbook) + return BaseResponse(message=f"Self-Healing Strategy Request Triggered") + + +@app.post( + "/update-iac-bundle/", + status_code=status.HTTP_201_CREATED, + response_model=BaseResponse, + tags=["deployments"], +) +async def self_healing_bundle( + d: DeploymentRequest, + background_tasks: BackgroundTasks, + _: APIKey = Depends(get_api_key), +): + i = Iem(credentials=d.credentials) + background_tasks.add_task(i.self_healing_bundle, d.deployment_id, d.bundle.base64) + return BaseResponse(message="Bundle Replacement Created") + + +if __name__ == "__main__": + uvicorn.run("main:app", host="127.0.0.1", port=8000, log_level="info") with open("../openapi.json", "w") as f: json.dump( get_openapi( diff --git a/iem-api/requirements.txt b/iem-api/requirements.txt index 00500ee..19fad26 100644 --- a/iem-api/requirements.txt +++ b/iem-api/requirements.txt @@ -1,7 +1,6 @@ fastapi==0.73.0 uvicorn==0.17.0.post1 -ansible==5.5.0 -ansible-core==2.12.3 +ansible==8.5.0 GitPython==3.1.26 requests==2.26.0 ratelimiter==1.2.0.post0 diff --git a/iem-api/src/__init__.py b/iem-api/src/__init__.py new file mode 100644 index 0000000..aa9f25d --- /dev/null +++ b/iem-api/src/__init__.py @@ -0,0 +1 @@ +from ._version import buildno, major, minor, revision diff --git a/iem-api/src/_version.py b/iem-api/src/_version.py new file mode 100644 index 0000000..9d0e591 --- /dev/null +++ b/iem-api/src/_version.py @@ -0,0 +1,4 @@ +major = 3 +minor = 0 +revision = 1 +buildno = 18 diff --git a/iem-api/src/core/engine.py b/iem-api/src/core/engine.py index 08f25cb..43cb59e 100644 --- a/iem-api/src/core/engine.py +++ b/iem-api/src/core/engine.py @@ -1,12 +1,12 @@ import logging import os import subprocess - +import time from abc import ABC, abstractmethod -from jinja2 import Template -from subprocess import CalledProcessError -LOGGER = logging.getLogger("iem") +from jinja2 import Environment, FileSystemLoader + +LOGGER = logging.getLogger(__name__) class Factory: @@ -26,6 +26,18 @@ class Engine(ABC): self._repo_path = repo_path self._env = env + def _run_command(self, args: list) -> subprocess.CompletedProcess: + output = subprocess.run( + args=args, cwd=self._repo_path, env=self._env, capture_output=True + ) + if output.returncode == 0: + LOGGER.info(output.stdout.decode("utf-8")) + LOGGER.info(output.stderr.decode("utf-8")) + else: + LOGGER.error(output.stdout.decode("utf-8")) + LOGGER.error(output.stderr.decode("utf-8")) + return output + @abstractmethod def apply(self): pass @@ -43,107 +55,80 @@ class Engine(ABC): class Terraform(Engine): - def __init__(self, repo_path, my_env): + def __init__(self, repo_path, my_env, skip_inventory=False): super().__init__(name="Terraform", repo_path=repo_path, env=my_env) def apply(self): LOGGER.info("About to apply terraform") - try: - output = subprocess.run( - ["terraform", "init"], - check=True, - cwd=self._repo_path, - env=self._env, - capture_output=True, - ) - output = subprocess.run( - ["terraform", "apply", "-auto-approve"], - check=True, - cwd=self._repo_path, - env=self._env, - # capture_output=True, - ) - return "CREATED", output.stdout, output.stderr - except CalledProcessError as e: - LOGGER.exception(e) - return "ERROR", None, None + + args = ["terraform", "init"] + output = self._run_command(args=args) + if output.returncode != 0: + return output.returncode, output.stdout, output.stderr + + args = ["terraform", "apply", "-auto-approve"] + output = self._run_command(args=args) + return output.returncode, output.stdout, output.stderr def destroy(self): - try: - output = subprocess.run( - ["terraform", "destroy", "-auto-approve"], - check=True, - cwd=self._repo_path, - env=self._env, - # capture_output=True, - ) - return "DESTROYED", output.stdout, output.stderr - except CalledProcessError as e: - LOGGER.exception(e) - return "ERROR", None, None + args = ["terraform", "destroy", "-auto-approve"] + output = self._run_command(args=args) + return output.returncode, output.stdout, output.stderr def output(self): - try: - output = subprocess.run( - ["terraform", "output", "-json"], - check=True, - cwd=self._repo_path, - env=self._env, - capture_output=True, - ) - return output.stdout - except CalledProcessError as e: - LOGGER.exception(e) - return None + args = ["terraform", "output", "-json"] + output = self._run_command(args=args) + output.check_returncode() + return output.stdout class Ansible(Engine): - def __init__(self, repo_path, my_env): + def __init__(self, repo_path, my_env, skip_inventory=False): super().__init__(name="Ansible", repo_path=repo_path, env=my_env) - self.__parse_inventory() + self.__parse_inventory() if not skip_inventory else None def __parse_inventory(self): - with open(f"{self._repo_path}/inventory.j2", "r") as f: - inventory = Template(f.read()) + environment = Environment(loader=FileSystemLoader({self._repo_path})) with open(f"{self._repo_path}/inventory", "w") as f: - f.write(inventory.render(self._env)) - - with open(f"{self._repo_path}/ssh_key.j2", "r") as f: - ssh_key = Template(f.read()) + template = environment.get_template("inventory.j2") + f.write(template.render(self._env)) with open(f"{self._repo_path}/ssh_key", "w") as f: - f.write(ssh_key.render(self._env)) + template = environment.get_template("ssh_key.j2") + f.write(template.render(self._env)) os.chmod(f"{self._repo_path}/ssh_key", 0o0600) def apply(self): LOGGER.info("About to apply ansible") - try: - output = subprocess.run( - ["ansible", "all", "-i", "inventory", "-m", "wait_for_connection"], - check=True, - cwd=self._repo_path, - env=self._env, - capture_output=True, - ) + for _ in range(2): + args = [ + "ansible", + "all", + "-i", + "inventory", + "-m", + "wait_for_connection", + ] + output = self._run_command(args=args) + if output.returncode != 0: + time.sleep(10) + continue LOGGER.info("All hosts in the inventory are reachable.") - output = subprocess.run( - ["ansible-playbook", "-i", "inventory", "main.yml"], - check=True, - cwd=self._repo_path, - env=self._env, - capture_output=True, - ) - return "CREATED", output.stdout, output.stderr - except CalledProcessError as e: - LOGGER.exception(e.output) - raise e + + args = ["ansible-playbook", "-i", "inventory", "main.yml"] + output = self._run_command(args=args) + if output.returncode != 0: + time.sleep(10) + continue + + return output.returncode, output.stdout, output.stderr def destroy(self): LOGGER.info("Nothing to be seen here.") - return "DESTROYED", None, None + return 0, None, None def output(self): LOGGER.info("Nothing to be seen here.") diff --git a/iem-api/src/core/iem.py b/iem-api/src/core/iem.py index e4ad215..8ba06ec 100644 --- a/iem-api/src/core/iem.py +++ b/iem-api/src/core/iem.py @@ -1,54 +1,38 @@ -import git +import base64 +import binascii import json import logging import os -import subprocess - -from git import GitCommandError, InvalidGitRepositoryError -from omegaconf import OmegaConf +import shutil +from io import BytesIO from subprocess import CalledProcessError +from zipfile import BadZipFile, ZipFile +from omegaconf import OmegaConf from src.core.engine import Factory -from src.core.persistence import Sqlite -from src.core.utils import DeploymentResponse, Credentials +from src.core.persistence import Persistence +from src.core.utils import Credentials, DeploymentResponse -LOGGER = logging.getLogger("iem") +LOGGER = logging.getLogger(__name__) class Iem: def __init__(self, credentials: Credentials = None): - logging.basicConfig( - level=logging.INFO, format="%(asctime)s %(levelname)-8s %(message)s" - ) - self._credentials = credentials - # if credentials.aws: - # self._aws_access_key_id = credentials.aws.access_key_id - # self._aws_secret_access_key = credentials.aws.secret_access_key - - # if credentials.openstack: - # self._os_username = credentials.openstack.user_name - # self._os_password = credentials.openstack.password - # self._os_auth_url = credentials.openstack.auth_url - # self._os_project_name = credentials.openstack.project_name - # Check IEM_HOME variable - if os.getenv("IEM_HOME") is None: - LOGGER.error("Please define IEM_HOME environment variable.") - exit(-1) - self._iem_home = os.getenv("IEM_HOME") + self._path_deployments = f"{os.environ['IEM_HOME']}deployments/" - self._persistence = Sqlite() + self._persistence = Persistence() def get_all_deployments(self): rows = self._persistence.get_all_deployments() for r in rows: d = DeploymentResponse( - status_time=r[0], - deployment_id=r[1], - status=r[2], - stdout=r[3], - stderr=r[4], + status_time=r.status_time, + deployment_id=r.deployment_id, + status=r.status, + stdout=r.stdout, + stderr=r.stderr, ) yield d @@ -56,67 +40,139 @@ class Iem: row = self._persistence.get_deployment(deployment_id=deployment_id) if row: d = DeploymentResponse( - status_time=row[0], - deployment_id=row[1], - status=row[2], - stdout=row[3], - stderr=row[4], + status_time=row.status_time, + deployment_id=row.deployment_id, + status=row.status, + stdout=row.stdout, + stderr=row.stderr, ) yield d - def deploy(self, deployment_id: str, repository: str, commit: str): + def get_deployment_outputs(self, deployment_id: str, stage_id: str): + function_name = "get_deployment_outputs" + LOGGER.info(f"Running {function_name} method.") + + repo_path = f"{self._path_deployments}{deployment_id}" + + LOGGER.info(f"Reading credentials.") + my_env = self._get_env( + deployment_id=deployment_id, credentials=self._credentials + ) + + LOGGER.info(f"About to read outputs for project {repo_path}.") + + conf = OmegaConf.load(f"{repo_path}/{stage_id}/config.yaml") + self.validate(env=my_env, io=conf.input) + + my_eng = Factory().get_engine(conf.engine)( + repo_path=f"{repo_path}/{stage_id}", + my_env=my_env, + ) + + output = my_eng.output() + + return dict(json.loads(output)) + + def _get_env(self, deployment_id: str, credentials: Credentials) -> dict: + my_env = os.environ.copy() + if credentials.aws: + my_env["AWS_ACCESS_KEY_ID"] = credentials.aws.access_key_id + my_env["AWS_SECRET_ACCESS_KEY"] = credentials.aws.secret_access_key + my_env["AWS_REGION"] = credentials.aws.region + if credentials.openstack: + my_env["OS_USERNAME"] = credentials.openstack.user_name + my_env["OS_PASSWORD"] = credentials.openstack.password + my_env["OS_AUTH_URL"] = credentials.openstack.auth_url + my_env["OS_PROJECT_NAME"] = credentials.openstack.project_name + if credentials.azure: + my_env["ARM_CLIENT_ID"] = credentials.azure.arm_client_id + my_env["ARM_CLIENT_SECRET"] = credentials.azure.arm_client_secret + my_env["ARM_SUBSCRIPTION_ID"] = credentials.azure.arm_subscription_id + my_env["ARM_TENANT_ID"] = credentials.azure.arm_tenant_id + if credentials.vmware: + my_env["VSPHERE_USER"] = credentials.vmware.user_name + my_env["VSPHERE_PASSWORD"] = credentials.vmware.password + my_env["VSPHERE_SERVER"] = credentials.vmware.server + my_env[ + "VSPHERE_ALLOW_UNVERIFIED_SSL" + ] = credentials.vmware.allow_unverified_ssl + if credentials.custom: + for key, value in credentials.custom.items(): + my_env[key] = value + + my_env["DEPLOYMENT_ID"] = deployment_id + + return my_env + + def _apply_stage( + self, my_env: dict, repo_path: str, stage: str, skip_inventory=False + ): + conf = OmegaConf.load(f"{repo_path}/{stage}/config.yaml") + + self.validate(env=my_env, io=conf.input) if not skip_inventory else None + + LOGGER.info(f"About to run stage: {stage}") + my_eng = Factory().get_engine(conf.engine)( + repo_path=f"{repo_path}/{stage}", + my_env=my_env, + skip_inventory=skip_inventory, + ) + returncode, stdout, stderr = my_eng.apply() + + if returncode == 0 and conf.output: + output = my_eng.output() + self.update_env(output=output, env=my_env, io=conf.output) + self.validate(env=my_env, io=conf.output) + + return returncode, stdout, stderr + + def deploy(self, deployment_id: str, bundle: str) -> int: function_name = "deploy" - LOGGER.info(f"Running {function_name} method") + LOGGER.info(f"Running {function_name} method.") self._persistence.insert_deployment( deployment_id=deployment_id, status="STARTED", stdout=None, stderr=None ) + repo_path = f"{self._path_deployments}{deployment_id}" + if not os.path.exists(repo_path): + os.makedirs(repo_path) + try: - repo_path = f"{self._iem_home}{deployment_id}" - self.get_repo(repo_path=repo_path, repository=repository, commit=commit) - - LOGGER.info(f"About to deploy project {repo_path}") - my_env = os.environ.copy() - if self._credentials.aws: - my_env["AWS_ACCESS_KEY_ID"] = self._credentials.aws.access_key_id - my_env[ - "AWS_SECRET_ACCESS_KEY" - ] = self._credentials.aws.secret_access_key - if self._credentials.openstack: - my_env["OS_USERNAME"] = self._credentials.openstack.user_name - my_env["OS_PASSWORD"] = self._credentials.openstack.password - my_env["OS_AUTH_URL"] = self._credentials.openstack.auth_url - my_env["OS_PROJECT_NAME"] = self._credentials.openstack.project_name + LOGGER.info(f"Decompressing base64 bundle.") + bundle_decode = base64.b64decode(bundle) + ZipFile(BytesIO(bundle_decode)).extractall(repo_path) + + LOGGER.info(f"Reading credentials.") + my_env = self._get_env( + deployment_id=deployment_id, credentials=self._credentials + ) + LOGGER.info(f"About to deploy project {repo_path}.") project_conf = OmegaConf.load(f"{repo_path}/config.yaml") for stage in project_conf.iac: - conf = OmegaConf.load(f"{repo_path}/{stage}/config.yaml") - - self.validate(env=my_env, io=conf.input) - - my_eng = Factory().get_engine(conf.engine)( - repo_path=f"{repo_path}/{stage}", my_env=my_env + returncode, stdout, stderr = self._apply_stage( + my_env=my_env, repo_path=repo_path, stage=stage ) - status, stdout, stderr = my_eng.apply() - - if conf.output: - output = my_eng.output() - self.update_env(output=output, env=my_env, io=conf.output) - self.validate(env=my_env, io=conf.output) + if returncode != 0: + LOGGER.error(f"Deployment failed for project {repo_path}.") + break self._persistence.insert_deployment( deployment_id=deployment_id, - status=status, + status="CREATED" if returncode == 0 else "FAILED", stdout=stdout, stderr=stderr, ) + LOGGER.info(f"Deployment completed for project {repo_path}") + return returncode except ( + binascii.Error, + BadZipFile, CalledProcessError, - GitCommandError, FileNotFoundError, - InvalidGitRepositoryError, NameError, + KeyError, ) as e: LOGGER.exception(e) self._persistence.insert_deployment( @@ -124,23 +180,15 @@ class Iem: ) raise e - LOGGER.info(f"The {function_name} method finished successfully") - def destroy(self, deployment_id: str): function_name = "destroy" LOGGER.info(f"Running {function_name} method") - repo_path = f"{self._iem_home}{deployment_id}" + repo_path = f"{self._path_deployments}{deployment_id}" - my_env = os.environ.copy() - if self._credentials.aws: - my_env["AWS_ACCESS_KEY_ID"] = self._credentials.aws.access_key_id - my_env["AWS_SECRET_ACCESS_KEY"] = self._credentials.aws.secret_access_key - if self._credentials.openstack: - my_env["OS_USERNAME"] = self._credentials.openstack.user_name - my_env["OS_PASSWORD"] = self._credentials.openstack.password - my_env["OS_AUTH_URL"] = self._credentials.openstack.auth_url - my_env["OS_PROJECT_NAME"] = self._credentials.openstack.project_name + my_env = self._get_env( + deployment_id=deployment_id, credentials=self._credentials + ) LOGGER.info(f"About to destroy project {repo_path}") project_conf = OmegaConf.load(f"{repo_path}/config.yaml") @@ -151,11 +199,19 @@ class Iem: my_eng = Factory().get_engine(conf.engine)( repo_path=f"{repo_path}/{stage}", my_env=my_env ) - status, stdout, stderr = my_eng.destroy() + returncode, stdout, stderr = my_eng.destroy() + if returncode != 0: + LOGGER.error(f"Undeployment failed for project {repo_path}.") + break + + if "/home" in repo_path: + LOGGER.exception(f"Cannot delete {repo_path}") + elif os.getenv("DOCKERIZED") == "true": + shutil.rmtree(repo_path) self._persistence.insert_deployment( deployment_id=deployment_id, - status=status, + status="DESTROYED" if returncode == 0 else "FAILED", stdout=stdout, stderr=stderr, ) @@ -174,20 +230,114 @@ class Iem: LOGGER.error(f"Variable {variable} not found in environment.") raise NameError - def get_repo(self, repo_path: str, repository: str, commit: str): - LOGGER.info( - f"About to download the project {repo_path} with repository {repository}" + def self_healing_strategy(self, deployment_id: str, playbook: str): + function_name = "self_healing_strategy" + LOGGER.info(f"Running {function_name} method") + + self._persistence.insert_deployment( + deployment_id=deployment_id, status="SHS-STARTED", stdout=None, stderr=None ) - if not os.path.isdir(repo_path): - os.makedirs(repo_path) - repo = git.Repo.clone_from( - url=repository, to_path=repo_path, no_checkout=True + + LOGGER.info(f"The {function_name} method finished successfully") + repo_path = f"{self._path_deployments}{deployment_id}" + if not os.path.exists(repo_path): + LOGGER.error( + f"The deployment_id = {deployment_id} does not correspond to any active deployment." ) - else: - repo = git.Repo(repo_path) + return - repo.git.checkout(commit) + try: + LOGGER.info(f"Updating main.yml with new playbook.") + with open(f"{repo_path}/self_healing_monitoring/main.yml", "w") as f: + f.write(playbook) - for submodule in repo.submodules: - submodule.update(init=True) - submodule.module().git.checkout() + LOGGER.info(f"Reading credentials.") + my_env = self._get_env( + deployment_id=deployment_id, credentials=self._credentials + ) + + returncode, stdout, stderr = self._apply_stage( + my_env=my_env, + repo_path=repo_path, + stage=f"self_healing_monitoring", + skip_inventory=True, + ) + if returncode != 0: + LOGGER.error(f"Update failed for project {repo_path}.") + + self._persistence.insert_deployment( + deployment_id=deployment_id, + status="UPDATED" if returncode == 0 else "FAILED", + stdout=stdout, + stderr=stderr, + ) + LOGGER.info(f"Update completed for project {repo_path}") + except ( + binascii.Error, + CalledProcessError, + FileNotFoundError, + NameError, + KeyError, + ) as e: + LOGGER.exception(e) + self._persistence.insert_deployment( + deployment_id=deployment_id, status="ERROR", stdout=None, stderr=None + ) + raise e + + def self_healing_bundle(self, deployment_id: str, bundle: str) -> int: + function_name = "self_healing_bundle" + LOGGER.info(f"Running {function_name} method") + + self._persistence.insert_deployment( + deployment_id=deployment_id, status="UPDATING", stdout=None, stderr=None + ) + + repo_path = f"{self._path_deployments}{deployment_id}" + if not os.path.exists(repo_path): + LOGGER.error( + f"The deployment_id = {deployment_id} does not correspond to any active deployment." + ) + return -1 + + try: + LOGGER.info(f"Decompressing base64 bundle.") + bundle_decode = base64.b64decode(bundle) + ZipFile(BytesIO(bundle_decode)).extractall(repo_path) + + LOGGER.info(f"Reading credentials.") + my_env = self._get_env( + deployment_id=deployment_id, credentials=self._credentials + ) + + LOGGER.info(f"About to update project {repo_path}.") + project_conf = OmegaConf.load(f"{repo_path}/config.yaml") + for stage in project_conf.iac: + returncode, stdout, stderr = self._apply_stage( + my_env=my_env, repo_path=repo_path, stage=stage, skip_inventory=True + ) + if returncode != 0: + LOGGER.error(f"Update failed for project {repo_path}.") + break + + self._persistence.insert_deployment( + deployment_id=deployment_id, + status="UPDATED" if returncode == 0 else "FAILED", + stdout=stdout, + stderr=stderr, + ) + LOGGER.info(f"Update completed for project {repo_path}") + return returncode + except ( + binascii.Error, + BadZipFile, + CalledProcessError, + FileNotFoundError, + NameError, + KeyError, + ) as e: + LOGGER.exception(e) + self._persistence.insert_deployment( + deployment_id=deployment_id, status="ERROR", stdout=None, stderr=None + ) + raise e diff --git a/iem-api/src/core/persistence.py b/iem-api/src/core/persistence.py index 2f4979e..10c3a2d 100644 --- a/iem-api/src/core/persistence.py +++ b/iem-api/src/core/persistence.py @@ -1,91 +1,21 @@ +import logging import os -import sqlite3 -# from dataclasses import dataclass -from datetime import datetime from ratelimiter import RateLimiter +from sqlalchemy import (Column, DateTime, Integer, String, create_engine, desc, + func, select) +from sqlalchemy.orm import Session, declarative_base -from sqlalchemy import create_engine -from sqlalchemy import Column -from sqlalchemy import DateTime, Integer, String -from sqlalchemy import select, desc -from sqlalchemy.orm import declarative_base, Session +LOGGER = logging.getLogger(__name__) Base = declarative_base() -class Sqlite: - def __init__(self): - self._db_name = "iem.db" - - # create database if it does not exist - if not os.path.isfile(self._db_name): - self.__create_database() - - def __create_database(self): - conn = sqlite3.connect(self._db_name) - with conn: - sql = """CREATE TABLE deployments (id INTEGER PRIMARY KEY AUTOINCREMENT, - status_time DATETIME DEFAULT CURRENT_TIMESTAMP, - deployment_id TEXT NOT NULL, status TEXT NOT NULL, stdout TEXT, stderr TEXT);""" - conn.execute(sql) - - def insert_deployment( - self, deployment_id: str, status: str, stdout: str, stderr: str - ): - conn = sqlite3.connect(self._db_name) - with conn: - sql = """INSERT INTO deployments (deployment_id, status, stdout, stderr) VALUES (?,?,?,?)""" - - cursor = conn.cursor() - cursor.execute( - sql, - ( - deployment_id, - status, - stdout, - stderr, - ), - ) - conn.commit() - - def get_deployment(self, deployment_id: str): - conn = sqlite3.connect(self._db_name) - with conn: - sql = """SELECT status_time, deployment_id, status, stdout, stderr FROM deployments - WHERE deployment_id=? ORDER BY id DESC LIMIT 1""" - - cursor = conn.cursor() - cursor.execute(sql, (deployment_id,)) - row = cursor.fetchone() - - return row - - def get_all_deployments(self): - conn = sqlite3.connect(self._db_name) - with conn: - sql = """SELECT status_time, deployment_id, status, stdout, stderr - FROM deployments ORDER BY id DESC LIMIT 25 OFFSET 0""" - - cursor = conn.cursor() - cursor.execute(sql) - rows = cursor.fetchall() - - return rows - - @RateLimiter(max_calls=10, period=1) - def valid_api_key(self, api_key_query: str): - if api_key_query == os.getenv("API_KEY"): - return True - else: - return False - - class Deployment(Base): __tablename__ = "deployments" id = Column(Integer, primary_key=True) - status_time = Column(DateTime, default=datetime.now()) + status_time = Column(DateTime, default=func.now()) deployment_id = Column(String, nullable=False) status = Column(String, nullable=False) stdout = Column(String) @@ -96,8 +26,8 @@ class Deployment(Base): class Persistence: - def __init__(self): - self._engine = create_engine("sqlite:///:memory:", future=True) + def __init__(self, engine_url="sqlite:///db/iem.db"): + self._engine = create_engine(url=engine_url, future=True) Base.metadata.create_all(self._engine) def insert_deployment( diff --git a/iem-api/src/core/utils.py b/iem-api/src/core/utils.py index 5ec5df2..4c4bcf1 100644 --- a/iem-api/src/core/utils.py +++ b/iem-api/src/core/utils.py @@ -1,10 +1,7 @@ -import logging - from datetime import datetime -from pydantic import BaseModel -from typing import Dict, Optional +from typing import Any, Dict, Optional -LOGGER = logging.getLogger("iem") +from pydantic import BaseModel class BaseResponse(BaseModel): @@ -22,6 +19,7 @@ class DeploymentResponse(BaseModel): class Aws(BaseModel): access_key_id: str secret_access_key: str + region: Optional[str] = "us-west-2" class Azure(BaseModel): @@ -42,19 +40,46 @@ class Openstack(BaseModel): user_domain_name: Optional[str] +class Docker(BaseModel): + server: str + user_name: str + password: str + + +class Vmware(BaseModel): + user_name: str + password: str + server: str + allow_unverified_ssl: Optional[str] + + class Credentials(BaseModel): aws: Optional[Aws] = None azure: Optional[Azure] = None openstack: Optional[Openstack] = None + vmware: Optional[Vmware] = None + docker: Optional[Docker] = None + custom: Optional[dict[str, Any]] = None + + +class Bundle(BaseModel): + base64: str class DeploymentRequest(BaseModel): deployment_id: str - repository: str - commit: str credentials: Credentials + bundle: Bundle + + +class SelfHealingRequest(BaseModel): + credentials: Credentials + playbook: str class DeleteDeploymentRequest(BaseModel): deployment_id: str credentials: Credentials + +class DeploymentStatusRequest(BaseModel): + credentials: Credentials diff --git a/iem-api/src/resources/id_iem b/iem-api/src/resources/id_iem new file mode 100644 index 0000000..2f5c307 --- /dev/null +++ b/iem-api/src/resources/id_iem @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/iem-api/src/resources/id_iem.pub b/iem-api/src/resources/id_iem.pub new file mode 100644 index 0000000..c4368a5 --- /dev/null +++ b/iem-api/src/resources/id_iem.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDV3VeYPD1WKQpq2PztNUQ0IAnm4JCmCPJpCLcem9kO3DvNbVBN5Q0JF0nfqgc8b1wP7uBH1b3G8jaX7Zsq6CpNu4OvXKw/EBxO3pdiAx6v0mRZMLBx2qcBd1TB90sxQ+juY6//06c52OgSjQ3Cey8a8/GKdtTMxRKh+rAuAch2WjcZBmTZPe+9E3fOhibHg5UA3sLhFcAPuEuEQRbTDBSsCqdCqHXfIe4NiLqJYVLOHkHuHfvTRaLROh7zmf20FmIP7s1gYU3GpDKRgZeuo2ZM9791N98B0JgSnepbx2/MDuG2WvAFwHIK7WSNVxGuxdvXspUWfDGam/G4ZGuxzaxVHtwYgSyxNoAUXcv82GS/MNLXqpfapvPR/LGcWAEcg1FgzjkC8y4MBZ6tQDowAYr/BEnUI7xmtIm0ixBWdd2zGtyMoSbvqHN0DSsRM+YDkD4XMcfd9Kp9kiIWHl1N9RSeV3D1JNfDGNGAE1qym25Ni0zWNbSyEUxpLkXj4T2wEDM= diff --git a/iem-api/tests/__init__.py b/iem-api/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iem-api/tests/it/__init__.py b/iem-api/tests/it/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iem-api/tests/it/test_it_iem.py b/iem-api/tests/it/test_it_iem.py new file mode 100644 index 0000000..529b412 --- /dev/null +++ b/iem-api/tests/it/test_it_iem.py @@ -0,0 +1,149 @@ +import base64 +import logging +import os +import unittest +import uuid + +from src.core.iem import Iem +from src.core.utils import Aws, Credentials, Openstack + +LOGGER = logging.getLogger(__name__) + + +class TestIem(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(TestIem, self).__init__(*args, **kwargs) + + # Check IEM_HOME variable + self._iem_home = os.environ["IEM_HOME"] + + @unittest.skipUnless(os.getenv("AWS"), "Define AWS variable to execute") + def test_deploy_destroy_aws(self): + + deployment_id = str(uuid.uuid4()) + a = Iem( + Credentials( + aws=Aws( + access_key_id=os.environ["AWS_ACCESS_KEY_ID"], + secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"], + ) + ) + ) + with open("tests/resources/aws.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + + a.deploy(deployment_id=deployment_id, bundle=bundle) + + a.destroy(deployment_id=deployment_id) + + @unittest.skipUnless(os.getenv("OS"), "Define OS variable to execute") + def test_deploy_destroy_openstack(self): + + deployment_id = str(uuid.uuid4()) + a = Iem( + Credentials( + openstack=Openstack( + user_name=os.environ["OS_USERNAME"], + password=os.environ["OS_PASSWORD"], + auth_url=os.environ["OS_AUTH_URL"], + project_name=os.environ["OS_PROJECT_NAME"], + ) + ) + ) + with open("tests/resources/openstack.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + + output = a.deploy(deployment_id=deployment_id, bundle=bundle) + + a.destroy(deployment_id=deployment_id) + + self.assertEqual(output, 0) + + @unittest.skipUnless(os.getenv("ANSIBLE"), "Define ANSIBLE variable to execute") + def test_deploy_destroy_ansible(self): + + deployment_id = str(uuid.uuid4()) + a = Iem( + Credentials( + openstack=Openstack( + user_name=os.environ["OS_USERNAME"], + password=os.environ["OS_PASSWORD"], + auth_url=os.environ["OS_AUTH_URL"], + project_name=os.environ["OS_PROJECT_NAME"], + ) + ) + ) + with open("tests/resources/ansible.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + + output = a.deploy(deployment_id=deployment_id, bundle=bundle) + + a.destroy(deployment_id=deployment_id) + + self.assertEqual(output, 0) + + @unittest.skipUnless( + os.getenv("INTEGRATION"), "Define INTEGRATION variable to execute" + ) + def test_deploy_destroy_docker(self): + + deployment_id = str(uuid.uuid4()) + a = Iem( + Credentials( + openstack=Openstack( + user_name=os.environ["OS_USERNAME"], + password=os.environ["OS_PASSWORD"], + auth_url=os.environ["OS_AUTH_URL"], + project_name=os.environ["OS_PROJECT_NAME"], + ) + ) + ) + with open("tests/resources/docker.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + + a.deploy(deployment_id=deployment_id, bundle=bundle) + + a.destroy(deployment_id=deployment_id) + + @unittest.skipUnless(os.getenv("SHS"), "Define SHS variable to execute") + def test_self_healing_strategy(self): + deployment_id = str(uuid.uuid4()) + a = Iem( + Credentials( + custom={ + "instance_usr": "vagrant", + "instance_pwd": "vagrant", + "instance_ip": "192.168.56.201", + } + ) + ) + with open("tests/resources/shs.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + + a.deploy(deployment_id=deployment_id, bundle=bundle) + + a.self_healing_strategy(deployment_id=deployment_id, strategy="25") + + @unittest.skipUnless(os.getenv("SHS"), "Define SHS variable to execute") + def test_self_healing_bundle(self): + deployment_id = str(uuid.uuid4()) + a = Iem( + Credentials( + aws=Aws( + access_key_id=os.environ["AWS_ACCESS_KEY_ID"], + secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"], + ) + ) + ) + with open("tests/resources/shs.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + + returncode = a.deploy(deployment_id=deployment_id, bundle=bundle) + self.assertEqual(returncode, 0) + + with open("tests/resources/shs-bundle.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + returncode = a.self_healing_bundle(deployment_id=deployment_id, bundle=bundle) + self.assertEqual(returncode, 0) + + a.destroy(deployment_id=deployment_id) diff --git a/iem-api/tests/resources/ansible.zip b/iem-api/tests/resources/ansible.zip new file mode 100644 index 0000000000000000000000000000000000000000..7d2d24ddd670766af89958f489b8cfceff66c99a GIT binary patch literal 3735 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 HaDjLLJSJi( literal 0 HcmV?d00001 diff --git a/iem-api/tests/resources/aws.zip b/iem-api/tests/resources/aws.zip new file mode 100644 index 0000000000000000000000000000000000000000..5813bd61d580f52708e4ed71dcb3a583fb107ddc GIT binary patch literal 1230 zcmWIWW@h1H00A%O@G!5g-~2KR3=AO5%^<^&oS&DLnXXrvn41$C!pXoaBl<mAUi5o% zX$3a}Bg+eB1_m&ptE;QPm6@1q#igL2t5A|!RFs&OUzE$Gr>Dn-upq%XJPc$J40AEa zFu>L7gUxpl{hkcMXy${=5Mf|o;9$u0j0}#R#2lK(z`)SOz`!7aVn%*xNkM5zv0h0U z*tEG3Wr<aot~Kp7<T`91!1BBL=pyzR6Yhl;Ogv{V&{cAf-A!oM<Y1QnwrZW*Wy_Y8 ztiJm|Wp%5}P3`Tyw~u<4RtQgsNS>S(WaPGEr+rxGj`JLvreUs?hs2JR?0z(5$K&m- zTaD|a1>;`%Tm2Ds{=2JB=&j2aMugk7yN-ngBwk;2fPsO*nvsD)7{%?$Ihm<>B`~Km z`qig^Fj_#k2HfU4WWZDVKD=d5b>gF^uJ#AxB{rUoNsLavrK$Am?Xu#-4vKTMGm`{Y zHEsD|#BgZ)LGIm~rWbtgQ|&WguvL;-F00VysMD+z5xizE@1^x2O~Uozi}_=p*{k2s z>HgbaIs4DOV`n}#+%$cr74oxf^YKDg=g9`zt;;v9(Ww;|E}bT5uqos0uk>Zt4%eGF zH6Fcn=~|v%p7+sr2dc{*176AmE=#xHwlwKxl{RDVrp|T$ozGwC>zXxNP3O{|V2jv~ ztcbw);v62vC(~--!@$50%fP@OiW(Sb=`LFAdvdxMM!LIv^=jyYQ)jd_bT<Y)2nx6m zTEN5KS6)(9#>?0Dy{xzAdr4zA+iolNoox!9MNg+sYcRc5xb)f5^k++>G^Z>P6IEUI zRQzet%a<<;85sh+8JXmmab*PwaH3^oU|`tN2x6jS3|2_SK#MD612L06#6SiH0fxVh zi3|)V8G)678EYm0IS^MifS3ny;8GOxkTV4<12ZU7V09_PWXwd0Y;qzavP+SYEGq*u vmgEWv0*Jww5sqwd9utbe$kETrzzm9h3^%i~fg*yHft6uBBLf2`Gl&NOz*ui; literal 0 HcmV?d00001 diff --git a/iem-api/tests/resources/docker.zip b/iem-api/tests/resources/docker.zip new file mode 100644 index 0000000000000000000000000000000000000000..888efb0b9697997713ed9d800eb25fcf8bf989c9 GIT binary patch literal 5400 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
lX&%#s*9Qw2m!z=FbU*l0<6}|H zspf>He}8qS99=!%#_B+_+y3J^vkz9Uy;fu_!C0pK!1&dUvLEqG4q_7(?lW85Uh;}n z-BIE88p)RH6{>3{CaEt-o0j@zdPkVo4g0M-Cua%EufA*49&=}LahGY__N286yWO=` zs%7^kgdOmcvfjWJb!>YOCr<@O5Yv&;+q_{RlkSO%%cWhP#_9A!VZtqsQv3U_?gh1^ zoHkqZNvEQs*jmH-O#Rh4rk|fKidge(zrjtL2cL^9k8l5!J}JsPXSuIv?)&I#CNBGh z`9qH{`53TV?2)DH;>M#FxP!%(H2Qq`$k}=!GnxO$WRFXdoyse9Qs$;yKc0OzBW<E! zin-)!m(Y}{cO^DV3_E#_ef{NKmOlIkc&>et^NuPjnN<C!T<mLpaLop(B(HgAqmwvZ zSD#h2GRnME(X;j4S#Q4iT{jwMADwut>8k0hYrmDgUsZm6@{a0N*C>r2AuDg@u+6uL zn$+fDIk~BM--0P}-&n49NjKi8m?FRAXVnhZTb5t<KE9RB-*R~y$J&XHT<_JE1|>Y$ zu}OWd=c>u4Z^<m2vc8+OHeN4$$=<NN*(z_?rIJs4tp2yG;Qk-hmgAxS-Yz)vZNs|g zbF4}wXEP_vFR4HDzW%Y<>gbhPPh4}I#oN{VDrPOdWEUd6xSh|`y54xk3iFh&K1*h# ziDlI_&bhzhP4fSrje?(QLUshV2B@SOnA!;l{QeVTVH+M4KF=UFyzq@$%?Z)xo>N(V z9(p_RQOuOZ84o`j?dEy^Sfq4r=k1e?iSdvB&su0~qvK$1|8HUc;xC=se=EN)Ut%}2 z#W2ft(NXn>$Aw=^+AioNG2L!i)?KIm?8sape%rmDN=lU9t?b%1;kDg+XXPu$^7w_q z_o@B1YE0hl`7-<3mynCj*E<*U^}A<fv`+l<MPu!befKZjdNcp7!0)}C$9A8s49XTS zc#u-~DyfmfIr%Qr>$cl&MVGv{zBFaqyiV_a?cX0SnPcMj?ce$9$Jgzz4qtsfTVLbr znpV&4&mQ)D{POm2ukFQRWos?h@&Z{kW((7VHn!<I%?!9Ky54`^>S%vgx6$SKQspRF zPfo$<a`#@Xc~;l$eXs0a+>CRnHW4io-{kf7pI^8)(q{qdBf(iu|BL2qtuk#CTlwGm zPLXA!Scg5En|TYOzJPRuTs{VUY-eC#@L^<N05u;Fr9^&dNkM6e9^rCgZm%~NlOa#r zcU#vP%pxW4Bb{^t50%WiB;HX`ep_%(q(;;2{~v3QW$11co1yZ=>7ZenU$5}7s&9`9 zo=IPmlU3Ux-Qc#*XpVz&|K8abUtaoe(>75h(fU?tzIj9CjQ6utdgH#=+e$xN?4h*n zsrt7^Q@dKS<Acw=N>SLoIR3uuJFklg<v$JV83Vi-nM9az_fi?4U`r#2fnKzO`l_H- z28hO}Eno&??}mbm1L41p6`(E=X`ND-(U90cZp?wYr66NLcuV79bfYmEcz8Og$Uy_D z8ju@apw1)63J~7X*uuzw+O0-wjDaixHN>#=gi$QP(nkbY0K!`unV8TlL5~bPJw_B; zu=MaiHh}P!##QLHfU7+s`g;f~uy-Ut#)I%*$7L)G43LHpq{#v5PT=fTAYvL@#{y&^ z2ybb8jW85FTtM9mP$C7<I1?!c1Ga7f$ZQba(%8!icQi&C0(A~R7Jz6x7GP_<gA51Z zEsY**a0}2<4=Ck;j0e$pjK|hS2N?~*TN)1_j7N!4JS}$Q7{%5|1{n;(e;wzr!`%*P z=Yktapr$g;CNm;lu{D}O#)9yc#^>lpLsAYYO=x6)U~2(`ECS)bj_n)_B(;YTp1{^3 z2AK)MTN=-!n2Vl1@wAT-PKT6q=&e(b@gTgVaTzDF)4@?pxDG-$9=%!v84bc)8qe?| b8&7m47vRmx1~Nd9L69Mije%haABYD4)j5y@ literal 0 HcmV?d00001 diff --git a/iem-api/tests/resources/dummy.zip b/iem-api/tests/resources/dummy.zip new file mode 100644 index 0000000000000000000000000000000000000000..20d2bc346232701a6a840e0834e7709fdc67bab3 GIT binary patch literal 3700 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 R2JkTOFodx%FmQki7y#hT4>te+ literal 0 HcmV?d00001 diff --git a/iem-api/tests/resources/main.yml b/iem-api/tests/resources/main.yml new file mode 100644 index 0000000..435d9f3 --- /dev/null +++ b/iem-api/tests/resources/main.yml @@ -0,0 +1,8 @@ +--- +- hosts: all + become: yes + tasks: + - name: install nmap + apt: + name: nmap + state: latest diff --git a/iem-api/tests/resources/openstack.zip b/iem-api/tests/resources/openstack.zip new file mode 100644 index 0000000000000000000000000000000000000000..ddadb660c3f9e3cbffc71126f543e8a9dd293689 GIT binary patch literal 2117 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 literal 0 HcmV?d00001 diff --git a/iem-api/tests/resources/shs-bundle.zip b/iem-api/tests/resources/shs-bundle.zip new file mode 100644 index 0000000000000000000000000000000000000000..afdb59146f1195c79fd82a8bd6f657ec1270b942 GIT binary patch literal 2223 zcmWIWW@h1H00Hrk$6*1iWnU#37#Kj9n?Z&lIX^EgGhMGTF*hePgp+}JUHhUG5H79Y zW?*D_!OXw_CUkXmxiS-zt+;d*N>Yo864UaFa=G;M^teE#i+~JgV5o`-gD@Z@7lQ%= zT&;d+2rmQsLY{xgti1n{K{T2LFmpH<S~Ks3MVdZh)?#2_&}U#^5M_`-G6&tgT|4Hb zfH0bIm#<!Z>UGxp{2A}9K^mUAT3$Y<^-n!Lbw*o5cVkdT-_uk2x{ZckzkJ!jfUwv+ z>v5QW>a)s43=9l!85kG@Q7q0)%*@j(NdpH`<hn&EAdF_IY42ISCIg=1@2k7g?m3!V zoE%rd=pQ$I=hO`rciu4g^4RUxPMD>3_W$GSa-Ai@E1LTm6l(bTF0ArMED_T@Yt&Gu zYO&Vbfs0wPWzC7O<=199Y&;{l^H;Lot`7<StJ`WfTI-!tsZdw^S25wvteufrbKKuW z<a~Kheexvlm6l!Z7Yg=XX)5@8*45Na+3`_y`NhXghUX^j`kC;~XVtc&HmRivxA;Mx zg`|a)lzS+##mgYWP@I~RrkjzPn3I{8t`GL;o%Qol9<QI5g62_#F{Nn62%sBdWC}6q z-}-qe%p0(p1WG;}44!e1!_-R@lpPru7(gzQ#$pnB<lmgRC<TPkoCk`0NK`+%6nf%} zww~u1Z@*rBT|K8~VpEnY6dQBz_~h;FP$jY`d1uh)OU9pr)`|#y?lPWS6twdTYMSVA zd>poB!6xNo1_p*z3=9l1SX`BvSC*Prl3!G*mt_PFwG(NJQa~8ZS-EE&xf&D%S|9FR z{(>u)*<VO#GM~x~4db-#7w=RXMYh!bXU}gDXk*fGW9*#I>}Mvl@T}b=vB+C{I5*k# zotvDZ5Y7JX+~%(=O4TXf4KvI0jTiU6cK$y@>V1FH9qF&@?5<WYA|hr>X-t^U%F{j? z3=9k)EQQ74#l;!%*{N`6H?#gr-h>%N)zu1_dBr7(dC95q#i>PQsYUSxMVV!ZC8;3I z@ukJ7MTQEsweXUFgW+Ms<FLiM#mb@>7#P|Z7#Jk5*bUB3mC!<GYyYAY5Jn3y-Cjqo z1_K_J_q9js6y#5&t6XzBdP_`*|KGikBF^2LnH3kgtZ?>~ycEUsZPM{riTuEPwJObO zg_6i0Z?g0j-8YhDHtPM~U~sKT;oYpo&%$+Y81t`>o)+`?EPH@ABa<96Bqv~&iW1-g zjFEwXVM`;3iIVJCA!Q_*Q$YrbFoRMu1J;5NWDE%Zb&O+RKq}4%7mFZ=g3QHMFd{p2 z8H%~+PR3G1f}AV|b}|P8X3>Xiv^*oSlff|uDhOE_n6VUs5Mv=hi&@A(jAdXDVEF6E z!^D7I<ghX@gNht1feVW*%mN45$Y69M!Nm_N12d@j!D=cbXfX>Ph^de?Fc*ud=mCtS z7=nZZ!~)E61la;(X0(tnGSx>dRahCAv6Ltf8z5ngnc<Obc!R|T<m}JNz}(FGFBwk| rVP<M%^D|g5eE?1<;4IF{z>Fn(2Y9oxf${_w0~bRpGXsMND~JaG{_T7^ literal 0 HcmV?d00001 diff --git a/iem-api/tests/resources/shs.zip b/iem-api/tests/resources/shs.zip new file mode 100644 index 0000000000000000000000000000000000000000..0541812d6c84e6b1f5d87cdd0cef5255b9a6a16d GIT binary patch literal 2169 zcmWIWW@h1H00Hrk$6*1iWnU#37#Kj9n?Z&lIX^EgGhMGTF*hePgp+}JUHhUG5H79Y zW?*D_!OXw_CUkXmxiS-zt+;d*N>Yo864UaFa=G;M^teE#i+~JgV5o`-gD@Z@7lQ%= zT&;d+2rmQsLY{xgti1n{K{T2LFmpH<S~Ks3MVdZh)?#2_&}U#^5M_`-G6&tgT|4Hb zfH0bIm#<!Z>UGxp{2A}9K^mUAT3$Y<^-n!Lbw*o5cVkdT-_uk2x{ZckzkJ!jfUvkX z>v33=bF^L#0|Ub}1_lN}6pM2cGxPLH(!hcAYu%z05Jt098)i}Hif2Iq8eY0OXLY=M zJM}eA>7VxT)YW3+*49v&Ca!(M=_VJ~(Vt6QPp;&05M0`G<WbL=Cw~?()h-ZHT~Q+$ zkepU0=dtgc-@%CUCuEd@PqQ96@l9dnmd&9CSBx(i2RvH!lnLx?NUWx$+(QX>UIrP4 z;?$fp-Hg=4oXos*eXx`7te=<ic>TN-G$(_64>6_`%@_f6V~k88CjDDKFNJvnR+B&p zfrG&_?s1rUiGs2t0|NudWztwoLXXCqGZ&?RFq-o~(FlpON0&lRoYB_vJmc-xtFNo) z^h|8Za)n}J?j4`Jy&bAV7A5Zt`h3awbI@85q0e2$lZ%3OenE}h9>>RFYZh!$PG(?W zSjE7=AcMtKnR#WYc_sNpm3mo5;7~h}wkQRJ(VUfg){(10L7?^F-sLa2a+&>wgeLQ; z+|V#i>wfW0wNYeC?SJ<C7J)V<9XH0#`OJQ1LJQB@O%jW|wTE+)UEjIMDGJf-@6K)h z%A!=A^4&1AJl}Y6?`!A(Go;@4H{Fr`y3X!u1tTJ2wv@(%`K&zcqrt$y0K!sO99~?U z5ucq3cXl)DzvNArQB+;6keOFpl9-pA8eg1RRF+y4Ur>};mROPs(i~q}oLXe4P+JSn z(;N&BBOZq>-Yr%Z#lXPO#=yWJfyHic#;AlAA6xqurGPM6fa&%+ay1z6u)ME5TBjg? zB3<R0)6rXELj3>kg%ok_-ps7H$Yq7IujHjDrf-vu$4cY}=Brg{Rx6Z5{&<t6x9Gl+ zEVEJX{|19=O$zU3Eq)fRd&8K2ee|@L$7k6CycwC~m?23RvwV{P7e$N=3=CTuK}?in z#|kO!(3}b~P=pzjk{PfTX&_@j_^)Fe0|QdQM7V$hITU0rwjvJMq03OrMRzim!VctQ zIk1yC7%&SlWTWL7k(~^VIZ%<t%D{}JID;4q30llT24XA&g8;){$7_rX=tT}I12d?| z!4kNz*upGukd2gOLNgLv{ID`GgNh%lrb2=iv+#kK3P}U$SWHC^U@XNDBqSgfV3s4u z7W~Cx0cxqj%D{}JM1j}<32V#@k8Hy>X3T&=&i<?n%+0L-lJNu)W~N3q--rd%d~iYm eXK_{rW-Qq|z?+o~lqa|txENZQ85r)dfOr5mWm{GN literal 0 HcmV?d00001 diff --git a/iem-api/tests/unit/__init__.py b/iem-api/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iem-api/tests/unit/test_iem.py b/iem-api/tests/unit/test_iem.py new file mode 100644 index 0000000..97fa04d --- /dev/null +++ b/iem-api/tests/unit/test_iem.py @@ -0,0 +1,31 @@ +import logging +import os +import unittest +import uuid + +from src.core.iem import Iem +from src.core.utils import Aws, Credentials, Openstack + +LOGGER = logging.getLogger(__name__) + + +class TestIem(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(TestIem, self).__init__(*args, **kwargs) + + # Check IEM_HOME variable + self._iem_home = os.environ["IEM_HOME"] + + def test_get_all_deployments(self): + i = Iem() + all_deployments = i.get_all_deployments() + self.assertGreaterEqual(len(list(all_deployments)), 0) + + def test_get_deployment_ok(self): + i = Iem() + i.get_deployment(deployment_id="deployment2") + + def test_get_deployment_not_ok(self): + i = Iem() + deployment_id = str(uuid.uuid4()) + i.get_deployment(deployment_id=deployment_id) diff --git a/iem-api/tests/unit/test_main.py b/iem-api/tests/unit/test_main.py new file mode 100644 index 0000000..fb4591d --- /dev/null +++ b/iem-api/tests/unit/test_main.py @@ -0,0 +1,224 @@ +import base64 +import os +import unittest +import uuid +from unittest.mock import Mock, patch + +from fastapi.testclient import TestClient +from main import app + + +class TestMain(unittest.TestCase): + def __init__(self, *args, **kwargs): + super(TestMain, self).__init__(*args, **kwargs) + + self.client = TestClient(app) + + # Check API_KEY variable + self._api_key = os.environ["API_KEY"] + + def test_root_no_apikey(self): + response = self.client.get("/") + assert response.status_code == 403 + + def test_root(self): + response = self.client.get("/", headers={"x-api-key": self._api_key}) + assert response.status_code == 200 + + def test_get_all_deployments(self): + response = self.client.get( + f"/deployments/", headers={"x-api-key": self._api_key} + ) + assert response.status_code == 200 + + def test_get_one_deployment(self): + response = self.client.get( + f"/deployments/25/", headers={"x-api-key": self._api_key} + ) + assert response.status_code == 200 + + @patch("subprocess.run") + def test_deployments(self, mock_run): + mock_run.return_value = Mock(returncode=0, stdout=b"{}", stderr=b"{}") + with open("tests/resources/dummy.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + response = self.client.post( + f"/deployments/", + headers={"x-api-key": self._api_key}, + json={ + "deployment_id": str(uuid.uuid4()), + "credentials": { + "openstack": { + "user_name": "string", + "password": "string", + "auth_url": "string", + "project_name": "string", + "region_name": "string", + "domain_name": "string", + "project_domain_name": "string", + "user_domain_name": "string", + } + }, + "bundle": {"base64": bundle.decode("utf-8")}, + }, + ) + assert response.status_code == 201 + + @patch("subprocess.run") + def test_undeploy(self, mock_run): + mock_run.return_value = Mock(returncode=0, stdout=b"{}", stderr=b"{}") + + deployment_id = str(uuid.uuid4()) + + with open("tests/resources/dummy.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + response = self.client.post( + f"/deployments/", + headers={"x-api-key": self._api_key}, + json={ + "deployment_id": deployment_id, + "credentials": { + "openstack": { + "user_name": "string", + "password": "string", + "auth_url": "string", + "project_name": "string", + "region_name": "string", + "domain_name": "string", + "project_domain_name": "string", + "user_domain_name": "string", + } + }, + "bundle": {"base64": bundle.decode("utf-8")}, + }, + ) + assert response.status_code == 201 + + response = self.client.post( + f"/undeploy/", + headers={"x-api-key": self._api_key}, + json={ + "deployment_id": deployment_id, + "credentials": {}, + }, + ) + assert response.status_code == 202 + + @patch("subprocess.run") + def test_custom(self, mock_run): + mock_run.return_value = Mock(returncode=0, stdout=b"{}", stderr=b"{}") + with open("tests/resources/dummy.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + response = self.client.post( + f"/deployments/", + headers={"x-api-key": self._api_key}, + json={ + "deployment_id": str(uuid.uuid4()), + "credentials": { + "openstack": { + "user_name": "string", + "password": "string", + "auth_url": "string", + "project_name": "string", + "region_name": "string", + "domain_name": "string", + "project_domain_name": "string", + "user_domain_name": "string", + }, + "custom": { + "CUSTOM_VAR1": "string", + "CUSTOM_VAR2": "string", + "CUSTOM_VAR3": "string", + }, + }, + "bundle": {"base64": bundle.decode("utf-8")}, + }, + ) + assert response.status_code == 201 + + @patch("subprocess.run") + def test_self_healing_strategy(self, mock_run): + mock_run.return_value = Mock(returncode=0, stdout=b"{}", stderr=b"{}") + + deployment_id = str(uuid.uuid4()) + with open("tests/resources/shs.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + + response = self.client.post( + f"/deployments/", + headers={"x-api-key": self._api_key}, + json={ + "deployment_id": deployment_id, + "credentials": {}, + "bundle": {"base64": bundle.decode("utf-8")}, + }, + ) + assert response.status_code == 201 + + response = self.client.post( + f"/self-healing/{25}", + headers={"x-api-key": self._api_key}, + json={ + "deployment_id": deployment_id, + "credentials": { + "custom": { + "instance_usr": "vagrant", + "instance_pwd": "vagrant", + "instance_ip": "192.168.56.201", + } + }, + }, + ) + assert response.status_code == 201 + + @patch("subprocess.run") + def test_self_healing_bundle(self, mock_run): + mock_run.return_value = Mock(returncode=0, stdout=b"{}", stderr=b"{}") + + deployment_id = str(uuid.uuid4()) + with open("tests/resources/dummy.zip", "rb") as binary_file: + bundle = base64.b64encode(binary_file.read()) + + response = self.client.post( + f"/deployments/", + headers={"x-api-key": self._api_key}, + json={ + "deployment_id": deployment_id, + "credentials": { + "openstack": { + "user_name": "string", + "password": "string", + "auth_url": "string", + "project_name": "string", + "region_name": "string", + "domain_name": "string", + "project_domain_name": "string", + "user_domain_name": "string", + } + }, + "bundle": {"base64": bundle.decode("utf-8")}, + }, + ) + assert response.status_code == 201 + + response = self.client.post( + f"/update-iac-bundle/", + headers={"x-api-key": self._api_key}, + json={ + "deployment_id": deployment_id, + "credentials": { + "openstack": { + "user_name": "string", + "password": "string", + "auth_url": "string", + "project_name": "string", + "region_name": "string", + "domain_name": "string", + "project_domain_name": "string", + "user_domain_name": "string", + } + }, + "bundle": {"base64": bundle.decode("utf-8")}, + }, + ) + assert response.status_code == 201 diff --git a/iem-api/tests/unit/test_persistence.py b/iem-api/tests/unit/test_persistence.py new file mode 100644 index 0000000..6683e8b --- /dev/null +++ b/iem-api/tests/unit/test_persistence.py @@ -0,0 +1,32 @@ +import unittest +import uuid + +from src.core.persistence import Persistence + + +class TestPersistence(unittest.TestCase): + @classmethod + def setUpClass(self): + self.engine_url = "sqlite:///:memory:" + + def test_insert_deployment(self): + Persistence(engine_url=self.engine_url).insert_deployment( + "deployment1", "STARTED", "stdout", "stderr" + ) + + def test_get_deployment(self): + p = Persistence(engine_url=self.engine_url) + p.insert_deployment("deployment2", "STOPPED", "stdout", "stderr") + p.insert_deployment("deployment2", "STARTED", "stdout", "stderr") + d = p.get_deployment("deployment2") + self.assertEqual(d.status, "STARTED") + + def test_get_deployment_not_ok(self): + row = Persistence(engine_url=self.engine_url).get_deployment(str(uuid.uuid4())) + self.assertIsNone(row) + + def test_get_all_deployments(self): + persistence = Persistence(engine_url=self.engine_url) + persistence.insert_deployment("deployment3", "STARTED", "stdout", "stderr") + rows = persistence.get_all_deployments() + self.assertGreaterEqual(len(list(rows)), 1) diff --git a/openapi.json b/openapi.json index 4727b5d..a127e22 100644 --- a/openapi.json +++ b/openapi.json @@ -1 +1 @@ -{"openapi": "3.0.2", "info": {"title": "IaC Execution Manager", "description": "IaC Execution Manager", "version": "0.1.15"}, "paths": {"/": {"get": {"tags": ["greeting"], "summary": "Read Root", "operationId": "read_root__get", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}}, "security": [{"APIKeyHeader": []}]}}, "/deployments/": {"get": {"tags": ["deployments"], "summary": "Read Status", "operationId": "read_status_deployments__get", "parameters": [{"required": false, "schema": {"title": "Start", "type": "integer", "default": 0}, "name": "start", "in": "query"}, {"required": false, "schema": {"title": "Count", "type": "integer", "default": 25}, "name": "count", "in": "query"}, {"required": false, "schema": {"title": "Start Date", "type": "string", "default": "1970-01-01"}, "name": "start_date", "in": "query"}, {"required": false, "schema": {"title": "End Date", "type": "string", "default": "2100-01-01"}, "name": "end_date", "in": "query"}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"title": "Response Read Status Deployments Get", "type": "array", "items": {"$ref": "#/components/schemas/DeploymentResponse"}}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyHeader": []}]}, "post": {"tags": ["deployments"], "summary": "Deploy", "operationId": "deploy_deployments__post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/DeploymentRequest"}}}, "required": true}, "responses": {"201": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BaseResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyHeader": []}]}}, "/deployments/{deployment_id}": {"get": {"tags": ["deployments"], "summary": "Read Status Deployment", "operationId": "read_status_deployment_deployments__deployment_id__get", "parameters": [{"required": true, "schema": {"title": "Deployment Id", "type": "string"}, "name": "deployment_id", "in": "path"}, {"required": false, "schema": {"title": "Start", "type": "integer", "default": 0}, "name": "start", "in": "query"}, {"required": false, "schema": {"title": "Count", "type": "integer", "default": 1}, "name": "count", "in": "query"}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"title": "Response Read Status Deployment Deployments Deployment Id Get", "type": "array", "items": {"$ref": "#/components/schemas/DeploymentResponse"}}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyHeader": []}]}}, "/undeploy/": {"post": {"tags": ["deployments"], "summary": "Undeploy", "operationId": "undeploy_undeploy__post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/DeleteDeploymentRequest"}}}, "required": true}, "responses": {"202": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BaseResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyHeader": []}]}}}, "components": {"schemas": {"Aws": {"title": "Aws", "required": ["access_key_id", "secret_access_key"], "type": "object", "properties": {"access_key_id": {"title": "Access Key Id", "type": "string"}, "secret_access_key": {"title": "Secret Access Key", "type": "string"}}}, "Azure": {"title": "Azure", "required": ["arm_client_id", "arm_client_secret", "arm_subscription_id", "arm_tenant_id"], "type": "object", "properties": {"arm_client_id": {"title": "Arm Client Id", "type": "string"}, "arm_client_secret": {"title": "Arm Client Secret", "type": "string"}, "arm_subscription_id": {"title": "Arm Subscription Id", "type": "string"}, "arm_tenant_id": {"title": "Arm Tenant Id", "type": "string"}}}, "BaseResponse": {"title": "BaseResponse", "required": ["message"], "type": "object", "properties": {"message": {"title": "Message", "type": "string"}}}, "Credentials": {"title": "Credentials", "type": "object", "properties": {"aws": {"$ref": "#/components/schemas/Aws"}, "azure": {"$ref": "#/components/schemas/Azure"}, "openstack": {"$ref": "#/components/schemas/Openstack"}}}, "DeleteDeploymentRequest": {"title": "DeleteDeploymentRequest", "required": ["deployment_id", "credentials"], "type": "object", "properties": {"deployment_id": {"title": "Deployment Id", "type": "string"}, "credentials": {"$ref": "#/components/schemas/Credentials"}}}, "DeploymentRequest": {"title": "DeploymentRequest", "required": ["deployment_id", "repository", "commit", "credentials"], "type": "object", "properties": {"deployment_id": {"title": "Deployment Id", "type": "string"}, "repository": {"title": "Repository", "type": "string"}, "commit": {"title": "Commit", "type": "string"}, "credentials": {"$ref": "#/components/schemas/Credentials"}}}, "DeploymentResponse": {"title": "DeploymentResponse", "required": ["status_time", "deployment_id", "status"], "type": "object", "properties": {"status_time": {"title": "Status Time", "type": "string", "format": "date-time"}, "deployment_id": {"title": "Deployment Id", "type": "string"}, "status": {"title": "Status", "type": "string"}, "stdout": {"title": "Stdout", "type": "string"}, "stderr": {"title": "Stderr", "type": "string"}}}, "HTTPValidationError": {"title": "HTTPValidationError", "type": "object", "properties": {"detail": {"title": "Detail", "type": "array", "items": {"$ref": "#/components/schemas/ValidationError"}}}}, "Openstack": {"title": "Openstack", "required": ["user_name", "password", "auth_url", "project_name"], "type": "object", "properties": {"user_name": {"title": "User Name", "type": "string"}, "password": {"title": "Password", "type": "string"}, "auth_url": {"title": "Auth Url", "type": "string"}, "project_name": {"title": "Project Name", "type": "string"}, "region_name": {"title": "Region Name", "type": "string"}, "domain_name": {"title": "Domain Name", "type": "string"}, "project_domain_name": {"title": "Project Domain Name", "type": "string"}, "user_domain_name": {"title": "User Domain Name", "type": "string"}}}, "ValidationError": {"title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", "properties": {"loc": {"title": "Location", "type": "array", "items": {"type": "string"}}, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}}}}, "securitySchemes": {"APIKeyHeader": {"type": "apiKey", "in": "header", "name": "x-api-key"}}}} \ No newline at end of file +{"openapi": "3.0.2", "info": {"title": "IaC Execution Manager", "description": "IaC Execution Manager", "version": "3.0.1.17"}, "paths": {"/": {"get": {"tags": ["greeting"], "summary": "Read Root", "operationId": "read_root__get", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {}}}}}, "security": [{"APIKeyHeader": []}]}}, "/deployments/": {"get": {"tags": ["deployments"], "summary": "Read Status", "operationId": "read_status_deployments__get", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"title": "Response Read Status Deployments Get", "type": "array", "items": {"$ref": "#/components/schemas/DeploymentResponse"}}}}}}, "security": [{"APIKeyHeader": []}]}, "post": {"tags": ["deployments"], "summary": "Deploy", "operationId": "deploy_deployments__post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/DeploymentRequest"}}}, "required": true}, "responses": {"201": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BaseResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyHeader": []}]}}, "/deployments/{deployment_id}": {"get": {"tags": ["deployments"], "summary": "Read Status Deployment", "operationId": "read_status_deployment_deployments__deployment_id__get", "parameters": [{"required": true, "schema": {"title": "Deployment Id", "type": "string"}, "name": "deployment_id", "in": "path"}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"title": "Response Read Status Deployment Deployments Deployment Id Get", "type": "array", "items": {"$ref": "#/components/schemas/DeploymentResponse"}}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyHeader": []}]}}, "/deployments/{deployment_id}/{stage_id}/outputs": {"get": {"tags": ["deployments"], "summary": "Read Deployment Outputs", "operationId": "read_deployment_outputs_deployments__deployment_id___stage_id__outputs_get", "parameters": [{"required": true, "schema": {"title": "Deployment Id", "type": "string"}, "name": "deployment_id", "in": "path"}, {"required": true, "schema": {"title": "Stage Id", "type": "string"}, "name": "stage_id", "in": "path"}], "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"title": "Response Read Deployment Outputs Deployments Deployment Id Stage Id Outputs Get", "type": "object"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyHeader": []}]}}, "/undeploy/": {"post": {"tags": ["deployments"], "summary": "Undeploy", "operationId": "undeploy_undeploy__post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/DeleteDeploymentRequest"}}}, "required": true}, "responses": {"202": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BaseResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyHeader": []}]}}, "/deployments/{deployment_id}/self-healing": {"post": {"tags": ["deployments"], "summary": "Self Healing Strategy", "operationId": "self_healing_strategy_deployments__deployment_id__self_healing_post", "parameters": [{"required": true, "schema": {"title": "Deployment Id", "type": "string"}, "name": "deployment_id", "in": "path"}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/SelfHealingRequest"}}}, "required": true}, "responses": {"201": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BaseResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyHeader": []}]}}, "/update-iac-bundle/": {"post": {"tags": ["deployments"], "summary": "Self Healing Bundle", "operationId": "self_healing_bundle_update_iac_bundle__post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/DeploymentRequest"}}}, "required": true}, "responses": {"201": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BaseResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}, "security": [{"APIKeyHeader": []}]}}}, "components": {"schemas": {"Aws": {"title": "Aws", "required": ["access_key_id", "secret_access_key"], "type": "object", "properties": {"access_key_id": {"title": "Access Key Id", "type": "string"}, "secret_access_key": {"title": "Secret Access Key", "type": "string"}, "region": {"title": "Region", "type": "string", "default": "us-west-2"}}}, "Azure": {"title": "Azure", "required": ["arm_client_id", "arm_client_secret", "arm_subscription_id", "arm_tenant_id"], "type": "object", "properties": {"arm_client_id": {"title": "Arm Client Id", "type": "string"}, "arm_client_secret": {"title": "Arm Client Secret", "type": "string"}, "arm_subscription_id": {"title": "Arm Subscription Id", "type": "string"}, "arm_tenant_id": {"title": "Arm Tenant Id", "type": "string"}}}, "BaseResponse": {"title": "BaseResponse", "required": ["message"], "type": "object", "properties": {"message": {"title": "Message", "type": "string"}}}, "Bundle": {"title": "Bundle", "required": ["base64"], "type": "object", "properties": {"base64": {"title": "Base64", "type": "string"}}}, "Credentials": {"title": "Credentials", "type": "object", "properties": {"aws": {"$ref": "#/components/schemas/Aws"}, "azure": {"$ref": "#/components/schemas/Azure"}, "openstack": {"$ref": "#/components/schemas/Openstack"}, "vmware": {"$ref": "#/components/schemas/Vmware"}, "docker": {"$ref": "#/components/schemas/Docker"}, "custom": {"title": "Custom", "type": "object"}}}, "DeleteDeploymentRequest": {"title": "DeleteDeploymentRequest", "required": ["deployment_id", "credentials"], "type": "object", "properties": {"deployment_id": {"title": "Deployment Id", "type": "string"}, "credentials": {"$ref": "#/components/schemas/Credentials"}}}, "DeploymentRequest": {"title": "DeploymentRequest", "required": ["deployment_id", "credentials", "bundle"], "type": "object", "properties": {"deployment_id": {"title": "Deployment Id", "type": "string"}, "credentials": {"$ref": "#/components/schemas/Credentials"}, "bundle": {"$ref": "#/components/schemas/Bundle"}}}, "DeploymentResponse": {"title": "DeploymentResponse", "required": ["status_time", "deployment_id", "status"], "type": "object", "properties": {"status_time": {"title": "Status Time", "type": "string", "format": "date-time"}, "deployment_id": {"title": "Deployment Id", "type": "string"}, "status": {"title": "Status", "type": "string"}, "stdout": {"title": "Stdout", "type": "string"}, "stderr": {"title": "Stderr", "type": "string"}}}, "Docker": {"title": "Docker", "required": ["server", "user_name", "password"], "type": "object", "properties": {"server": {"title": "Server", "type": "string"}, "user_name": {"title": "User Name", "type": "string"}, "password": {"title": "Password", "type": "string"}}}, "HTTPValidationError": {"title": "HTTPValidationError", "type": "object", "properties": {"detail": {"title": "Detail", "type": "array", "items": {"$ref": "#/components/schemas/ValidationError"}}}}, "Openstack": {"title": "Openstack", "required": ["user_name", "password", "auth_url", "project_name"], "type": "object", "properties": {"user_name": {"title": "User Name", "type": "string"}, "password": {"title": "Password", "type": "string"}, "auth_url": {"title": "Auth Url", "type": "string"}, "project_name": {"title": "Project Name", "type": "string"}, "region_name": {"title": "Region Name", "type": "string"}, "domain_name": {"title": "Domain Name", "type": "string"}, "project_domain_name": {"title": "Project Domain Name", "type": "string"}, "user_domain_name": {"title": "User Domain Name", "type": "string"}}}, "SelfHealingRequest": {"title": "SelfHealingRequest", "required": ["credentials", "playbook"], "type": "object", "properties": {"credentials": {"$ref": "#/components/schemas/Credentials"}, "playbook": {"title": "Playbook", "type": "string"}}}, "ValidationError": {"title": "ValidationError", "required": ["loc", "msg", "type"], "type": "object", "properties": {"loc": {"title": "Location", "type": "array", "items": {"type": "string"}}, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}}}, "Vmware": {"title": "Vmware", "required": ["user_name", "password", "server"], "type": "object", "properties": {"user_name": {"title": "User Name", "type": "string"}, "password": {"title": "Password", "type": "string"}, "server": {"title": "Server", "type": "string"}, "allow_unverified_ssl": {"title": "Allow Unverified Ssl", "type": "string"}}}}, "securitySchemes": {"APIKeyHeader": {"type": "apiKey", "in": "header", "name": "x-api-key"}}}} \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..9f5f8e0 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,5 @@ +sonar.projectKey=piacere_private_t51-iem_AXlg6OYJGykB3kuTt_u4 +sonar.qualitygate.wait=true +sonar.sources=iem-api/src +sonar.python.coverage.reportPaths=iem-api/coverage.xml + -- GitLab