aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTommy Carpenter <tommy@research.att.com>2019-05-29 13:36:01 -0400
committerTommy Carpenter <tommy@research.att.com>2019-06-04 09:12:25 -0400
commite14b49ead38227ff17d760c4771d58d9c6d2e7c0 (patch)
tree9e3cdc16376a5fb5f4b825a3930b28a89f58bccd
parent040d03d77587ce24f0e99ee504b5b0ff5473a39e (diff)
Switch to gevent
Issue-ID: DCAEGEN2-1549 Change-Id: I762d9630f857a23b6ae61992d483cdca7bb6f88d Signed-off-by: Tommy Carpenter <tommy@research.att.com>
-rw-r--r--Changelog.md3
-rw-r--r--Dockerfile28
-rw-r--r--README.md76
-rw-r--r--app/app/pom.xml270
-rw-r--r--app/app/tox.ini26
-rw-r--r--app/pom.xml80
-rw-r--r--app/uwsgi.ini3
-rw-r--r--config_binding_service/__init__.py (renamed from app/app/config_binding_service/__init__.py)0
-rw-r--r--config_binding_service/client.py (renamed from app/app/config_binding_service/client.py)0
-rw-r--r--config_binding_service/controller.py (renamed from app/app/config_binding_service/controller.py)0
-rw-r--r--config_binding_service/logging.py (renamed from app/app/config_binding_service/logging.py)127
-rw-r--r--config_binding_service/openapi.yaml (renamed from app/app/config_binding_service/openapi.yaml)2
-rwxr-xr-xconfig_binding_service/run.py (renamed from app/app/main.py)18
-rw-r--r--pom.xml232
-rw-r--r--setup.py (renamed from app/app/setup.py)14
-rw-r--r--tests/__init__.py (renamed from app/app/tests/__init__.py)0
-rw-r--r--tests/conftest.py (renamed from app/app/tests/conftest.py)0
-rw-r--r--tests/test_api.py (renamed from app/app/tests/test_api.py)0
-rw-r--r--tests/test_client.py (renamed from app/app/tests/test_client.py)0
-rw-r--r--tox.ini43
-rw-r--r--version.properties2
21 files changed, 377 insertions, 547 deletions
diff --git a/Changelog.md b/Changelog.md
index 136d04e..415c15b 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
+## [2.4.0] - 5/29/2019
+* Switch from NGINX to Gevent. The CBS is not CPU bound, and doesn't make any non-network blocking calls, so we don't really need a threaded server; an asyncronous event loop is fine. Gevent handles the patching of requests. The benefits of this are twofold; it will be easier to add https/http switching support, and it will be much easier to run as non-root in the Dockerfile. Moreover, it's "as fast" because again the CBS is not at all CPU bound so threading really doesn't buy anything over an async loop. This also has the practical benefit of 1 pom.xml instead of 3!
+
## [2.3.0] - 2/20/2019
* Expose the pretty UI at /ui
* Convert from swagger to openapi3
diff --git a/Dockerfile b/Dockerfile
index 6c783f0..1e65511 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,30 +1,14 @@
-FROM tiangolo/uwsgi-nginx-flask:python3.6
+FROM python:3.6
MAINTAINER tommy@research.att.com
-#setup uwsgi+nginx
-# https://hub.docker.com/r/tiangolo/uwsgi-nginx-flask/
-COPY ./app /app
+COPY . /tmp
+WORKDIR /tmp
RUN pip install --upgrade pip
-RUN pip install /app/app
-
+RUN pip install .
RUN mkdir -p /opt/logs/
-
-# create the dir for the ssl certs
-RUN mkdir -p /etc/nginx/ssl
-
-COPY nginxhttps.conf /etc/nginx/conf.d/nginxhttps.conf
-
-#443 is https, 10000 is http
-# in the future, hopefully http can go away completely
-ENV LISTEN_PORT 10000
-EXPOSE 443
EXPOSE 10000
-# Mount a self signed certificate that should be overwritten upon Run
-RUN apt-get update && \
- apt-get install -y openssl && \
- openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.crt -subj "/C=US/ST=NJ/L=foo/O=ONAP/OU=ONAP/CN=configbinding"
+ENV PROD_LOGGING 1
-#this is a registrator flag that tells it to ignore 80 from service discovery. Nothing is listening on 80, but the parent Dockerfile here exposes it. This container is internally listening on 10000 and 443.
-ENV SERVICE_80_IGNORE true
+CMD run.py
diff --git a/README.md b/README.md
index c639dab..a4e69ab 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ There is also a "dmaap key", which is the same concept, except what gets injecte
In addition, this service provides the capability to retrieve either the DTI events (not history) or the policies for a given service_component.
# Usage
-See the Swagger spec.
+See the OpenAPI spec in `config_binding_service/openapi.yaml`. You can also see a "pretty" version of this by running the container and going to `/ui`.
# Assumptions
1. `CONSUL_HOST` is set as an environmental variable where this binding service is run. If it is not, it defaults to the Rework Consul which is probably not what you want.
@@ -56,80 +56,34 @@ X's configuration:
}
```
-# A note about directory structure
-This project uses https://hub.docker.com/r/tiangolo/uwsgi-nginx-flask/
-This is a solution that runs a productionalized setup using NGINX+uwsgi+Flask (Flask is not meant to be run as a real webserver per their docs). This project requires the app/app structure. Tox still works from the root due to tox magic.
-
-This structure, combined with Sonar limitations, leads to an unfortunate need of having three nested poms. There is a top level pom, a tiny pom in /app, and the "main" pom in /app/app.
-
# Development
## Version changes
-An unforunate consequence of the nested poms is that development changes require a version bump in several places. They are:
+Development changes require a version bump in several places. They are:
1. Changelod.md
2. version.properties
-3. top level pom
-4. pom in /app
-5. pom in /app/app
-6. setup.py in /app/app
+3. pom.xml
+4. setup.py
Additionally, if the development leads to an API change,
-7. openapi.yaml in /app/app/config_binding_service
+5. config_binding_service/openapi.yaml
-## Testing
-You need `tox`.
+## Unit esting
+You need `tox`; then just run:
-To recreate the tox that the ONAP build process calls, from /app/app, *not in a virtual env*, just run:
-```
-tox
-```
-
-For local development, there is a tox that outputs to an html website that is easier to read and navigate then xml. From the *root*, run
-```
-tox -c tox-local.ini
-```
+ tox
# Deployment
-## Ports, HTTPS key/cert location
-
-The CBS frontend (NGINX) exposes 10000 and 443. It runs HTTP on 10000 and HTTPS on 443. 80 is also exposed by the parent Dockerfile but nothing is listening there so it can be ignored.
-
-The dockerimage mounts it's own self signed certificate. If deploying into a production level scenario, *you should overwrite this cert!*! It expects a key to be mounted at `/etc/nginx/ssl/nginx.key` and a cert to be mounted at `/etc/nginx/ssl/nginx.crt`. For example, a snippet from a `docker run` command:
-
-```
-... -v /host/path/to/nginx.key:/etc/nginx/ssl/nginx.key -v /host/path/to/nginx.crt:/etc/nginx/ssl/nginx.crt ...
-```
-
-These ports can be mapped to whatever extnernally. To keep the legacy behavior of prior ONAP releases of HTTP on 10000, map 10000:10000. Or, you can now make 10000 HTTPS by mapping 10000:443. This is determined by the deployment blueprint.
+## HTTPS
+Details coming soon
-## Non-K8, Registrator, Consul setup
-This section only pertains to a very specific setup of using Registrator and Consul (registrator to register a Consul healthcheck, and relying on Consul health checking). This section does *not* pertain to a Kubernetes deployment that uses K8 "readiness probes" instead of Consul.
+## Docker
-There is a combination of issues, rooting from a bug in registrator:
-1. https://jira.onap.org/browse/DCAEGEN2-482
-2. https://github.com/gliderlabs/registrator/issues/605
-
-That causes the Consul registration to be suffixed with ports, breaking the expected service name (`config_binding_service`), **even if** those ports are not mapped externally. That is, even if only one of the two ports (10000,443) is mapped, due to the above-linked bug, the service name will be wrong in Consul.
-
-The solution is to run the container with a series of ENV variables. If you want the healthchecks to go over HTTPS, you also need to run the latest version on `master` in registrator. The old (3 year old) release of `v7` does not allow for HTTPS healthchecks. The below example fixes the service name, turns OFF HTTP healthchecks, and turns ON HTTPS healthchecks (only works with latest registrator):
-
-```
-ENV SERVICE_10000_IGNORE true
-ENV SERVICE_443_NAME config_binding_service
-ENV SERVICE_443_CHECK_HTTPS /healthcheck
-ENV SERVICE_443_CHECK_INTERVAL 15s
-```
-
-E.g., in Docker run terminology:
-
-```
-... -e SERVICE_10000_IGNORE=true -e SERVICE_443_NAME=config_binding_service -e SERVICE_443_CHECK_HTTPS=/healthcheck -e SERVICE_443_CHECK_INTERVAL=15s ...
-```
+ sudo docker run -dt -p 10000:10000 -e CONSUL_HOST=<YOUR_HOST>cbs:X.Y.Z
If you wish to turn ON HTTP healthchecks and turn OFF HTTPS healthchecks, swith 10000 and 443 above. That will work even with `v7` of registrator (that is, `SERVICE_x_CHECK_HTTP` was already supported)
-## Running locally for development (no docker)
+## Locally for development (no docker)
It is recommended that you do this step in a virtualenv.
(set -x is Fish notaion, change for Bash etc. accordingly)
-```
-pip install --ignore-installed .; set -x CONSUL_HOST <YOUR_HOST>; ./main.py
-```
+
+ pip install --ignore-installed .; set -x CONSUL_HOST <YOUR_HOST>; ./run.py
diff --git a/app/app/pom.xml b/app/app/pom.xml
deleted file mode 100644
index 3b634a6..0000000
--- a/app/app/pom.xml
+++ /dev/null
@@ -1,270 +0,0 @@
-<?xml version="1.0"?>
-<!--
-================================================================================
-Copyright (c) 2017-2019 AT&T Intellectual Property. All rights reserved.
-================================================================================
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-============LICENSE_END=========================================================
-
-ECOMP is a trademark and service mark of AT&T Intellectual Property.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>org.onap.dcaegen2.platform.configbinding</groupId>
- <artifactId>app</artifactId>
- <version>2.3.0-SNAPSHOT</version>
- </parent>
- <!--- CHANGE THE FOLLOWING 3 OBJECTS for your own repo -->
- <groupId>org.onap.dcaegen2.platform.configbinding</groupId>
- <artifactId>app-app</artifactId>
- <name>dcaegen2-platform-configbinding-app-app</name>
- <version>2.3.0-SNAPSHOT</version>
- <url>http://maven.apache.org</url>
- <properties>
- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- <sonar.sources>.</sonar.sources>
- <sonar.junit.reportsPath>xunit-results.xml</sonar.junit.reportsPath>
- <sonar.python.coverage.reportPath>coverage.xml</sonar.python.coverage.reportPath>
- <sonar.python.xunit.reportPath>xunit-results.xml</sonar.python.xunit.reportPath>
- <sonar.language>py</sonar.language>
- <sonar.pluginname>python</sonar.pluginname>
- <sonar.inclusions>config_binding_service/*.py</sonar.inclusions>
- <sonar.exclusions>tests/*,setup.py</sonar.exclusions>
- </properties>
- <build>
- <finalName>${project.artifactId}-${project.version}</finalName>
- <pluginManagement>
- <plugins>
- <!-- the following plugins are invoked from oparent, we do not need them -->
- <plugin>
- <groupId>org.sonatype.plugins</groupId>
- <artifactId>nexus-staging-maven-plugin</artifactId>
- <version>1.6.7</version>
- <configuration>
- <skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo>
- </configuration>
- </plugin>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-deploy-plugin</artifactId>
- <!-- This version supports the "deployAtEnd" parameter -->
- <version>2.8</version>
- <configuration>
- <skip>true</skip>
- </configuration>
- </plugin>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-enforcer-plugin</artifactId>
- <version>3.0.0-M1</version>
- <configuration>
- <skip>true</skip>
- </configuration>
- </plugin>
- <!-- first disable the default Java plugins at various stages -->
- <!-- maven-resources-plugin is called during "*resource" phases by default behavior. it prepares the resources
- dir. we do not need it -->
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-resources-plugin</artifactId>
- <version>2.6</version>
- <configuration>
- <skip>true</skip>
- </configuration>
- </plugin>
- <!-- maven-compiler-plugin is called during "compile" phases by default behavior. we do not need it -->
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <version>3.1</version>
- <configuration>
- <skip>true</skip>
- </configuration>
- </plugin>
- <!-- maven-jar-plugin is called during "compile" phase by default behavior. we do not need it -->
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-jar-plugin</artifactId>
- <version>2.4</version>
- <executions>
- <execution>
- <id>default-jar</id>
- <phase/>
- </execution>
- </executions>
- </plugin>
- <!-- maven-install-plugin is called during "install" phase by default behavior. it tries to copy stuff under
- target dir to ~/.m2. we do not need it -->
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-install-plugin</artifactId>
- <version>2.4</version>
- <configuration>
- <skip>true</skip>
- </configuration>
- </plugin>
- <!-- maven-surefire-plugin is called during "test" phase by default behavior. it triggers junit test.
- we do not need it -->
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-surefire-plugin</artifactId>
- <version>2.12.4</version>
- <configuration>
- <skipTests>true</skipTests>
- </configuration>
- </plugin>
- <plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>exec-maven-plugin</artifactId>
- <version>1.2.1</version>
- <configuration>
- <executable>${session.executionRootDirectory}/mvn-phase-script.sh</executable>
- <environmentVariables>
- <!-- make mvn properties as env for our script -->
- <MVN_PROJECT_GROUPID>${project.groupId}</MVN_PROJECT_GROUPID>
- <MVN_PROJECT_ARTIFACTID>${project.artifactId}</MVN_PROJECT_ARTIFACTID>
- <MVN_PROJECT_VERSION>${project.version}</MVN_PROJECT_VERSION>
- <MVN_NEXUSPROXY>${onap.nexus.url}</MVN_NEXUSPROXY>
- <MVN_RAWREPO_BASEURL_UPLOAD>${onap.nexus.rawrepo.baseurl.upload}</MVN_RAWREPO_BASEURL_UPLOAD>
- <MVN_RAWREPO_BASEURL_DOWNLOAD>${onap.nexus.rawrepo.baseurl.download}</MVN_RAWREPO_BASEURL_DOWNLOAD>
- <MVN_RAWREPO_SERVERID>${onap.nexus.rawrepo.serverid}</MVN_RAWREPO_SERVERID>
- <MVN_DOCKERREGISTRY_DAILY>${onap.nexus.dockerregistry.daily}</MVN_DOCKERREGISTRY_DAILY>
- <MVN_DOCKERREGISTRY_RELEASE>${onap.nexus.dockerregistry.release}</MVN_DOCKERREGISTRY_RELEASE>
- </environmentVariables>
- </configuration>
- </plugin>
- </plugins>
- </pluginManagement>
- <plugins>
- <!-- plugin>
- <artifactId>maven-assembly-plugin</artifactId>
- <version>2.4.1</version>
- <configuration>
- <descriptors>
- <descriptor>assembly/dep.xml</descriptor>
- </descriptors>
- </configuration>
- <executions>
- <execution>
- <id>make-assembly</id>
- <phase>package</phase>
- <goals>
- <goal>single</goal>
- </goals>
- </execution>
- </executions>
- </plugin -->
- <!-- now we configure custom action (calling a script) at various lifecycle phases -->
- <plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>exec-maven-plugin</artifactId>
- <version>1.2.1</version>
- <executions>
- <execution>
- <id>clean phase script</id>
- <phase>clean</phase>
- <goals>
- <goal>exec</goal>
- </goals>
- <configuration>
- <arguments>
- <argument>__</argument>
- <argument>clean</argument>
- </arguments>
- </configuration>
- </execution>
- <execution>
- <id>generate-sources script</id>
- <phase>generate-sources</phase>
- <goals>
- <goal>exec</goal>
- </goals>
- <configuration>
- <arguments>
- <argument>__</argument>
- <argument>generate-sources</argument>
- </arguments>
- </configuration>
- </execution>
- <execution>
- <id>compile script</id>
- <phase>compile</phase>
- <goals>
- <goal>exec</goal>
- </goals>
- <configuration>
- <arguments>
- <argument>__</argument>
- <argument>compile</argument>
- </arguments>
- </configuration>
- </execution>
- <execution>
- <id>package script</id>
- <phase>package</phase>
- <goals>
- <goal>exec</goal>
- </goals>
- <configuration>
- <arguments>
- <argument>__</argument>
- <argument>package</argument>
- </arguments>
- </configuration>
- </execution>
- <execution>
- <id>test script</id>
- <phase>test</phase>
- <goals>
- <goal>exec</goal>
- </goals>
- <configuration>
- <arguments>
- <argument>__</argument>
- <argument>test</argument>
- </arguments>
- </configuration>
- </execution>
- <execution>
- <id>install script</id>
- <phase>install</phase>
- <goals>
- <goal>exec</goal>
- </goals>
- <configuration>
- <arguments>
- <argument>__</argument>
- <argument>install</argument>
- </arguments>
- </configuration>
- </execution>
- <execution>
- <id>deploy script</id>
- <phase>deploy</phase>
- <goals>
- <goal>exec</goal>
- </goals>
- <configuration>
- <arguments>
- <argument>__</argument>
- <argument>deploy</argument>
- </arguments>
- </configuration>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/app/app/tox.ini b/app/app/tox.ini
deleted file mode 100644
index 40fa3b3..0000000
--- a/app/app/tox.ini
+++ /dev/null
@@ -1,26 +0,0 @@
-# content of: tox.ini , put in same dir as setup.py
-[tox]
-envlist = py36,flake8
-
-[testenv]
-deps=
- pytest
- coverage
- pytest-cov
-setenv =
- CONSUL_HOST = 8.8.8.8
- HOSTNAME = config_binding_service
- PYTHONPATH={toxinidir}
-commands=
- pytest --junitxml xunit-results.xml --cov config_binding_service --cov-report xml --cov-report term --cov-fail-under=70
- coverage xml -i
-
-[testenv:flake8]
-basepython = python3.6
-skip_install = true
-deps = flake8
-commands = flake8 setup.py config_binding_service tests
-
-[flake8]
-ignore = E501,W605
-
diff --git a/app/pom.xml b/app/pom.xml
deleted file mode 100644
index 3e9e65f..0000000
--- a/app/pom.xml
+++ /dev/null
@@ -1,80 +0,0 @@
-<?xml version="1.0"?>
-<!--
-================================================================================
-Copyright (c) 2017-2019 AT&T Intellectual Property. All rights reserved.
-================================================================================
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-============LICENSE_END=========================================================
-
-ECOMP is a trademark and service mark of AT&T Intellectual Property.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>org.onap.dcaegen2.platform</groupId>
- <artifactId>configbinding</artifactId>
- <version>2.3.0-SNAPSHOT</version>
- </parent>
-
- <!--- CHANGE THE FOLLOWING 3 OBJECTS for your own repo -->
- <groupId>org.onap.dcaegen2.platform.configbinding</groupId>
- <artifactId>app</artifactId>
- <name>dcaegen2-platform-configbinding-app</name>
- <version>2.3.0-SNAPSHOT</version>
- <url>http://maven.apache.org</url>
-
- <packaging>pom</packaging>
- <modules>
- <module>app</module>
- </modules>
-
- <properties>
- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- <!-- customize the SONARQUBE URL -->
- <sonar.host.url>http://localhost:9000</sonar.host.url>
- <!-- taken care of in the children -->
- </properties>
- <build>
- <finalName>${project.artifactId}-${project.version}</finalName>
- <pluginManagement>
- <plugins>
- <plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>exec-maven-plugin</artifactId>
- <version>1.2.1</version>
- <configuration>
- <executable>${session.executionRootDirectory}/mvn-phase-script.sh</executable>
- <environmentVariables>
- <!-- make mvn properties as env for our script -->
- <!-- make mvn properties as env for our script -->
- <MVN_PROJECT_GROUPID>${project.groupId}</MVN_PROJECT_GROUPID>
- <MVN_PROJECT_ARTIFACTID>${project.artifactId}</MVN_PROJECT_ARTIFACTID>
- <MVN_PROJECT_VERSION>${project.version}</MVN_PROJECT_VERSION>
- <MVN_NEXUSPROXY>${onap.nexus.url}</MVN_NEXUSPROXY>
- <MVN_RAWREPO_BASEURL_UPLOAD>${onap.nexus.rawrepo.baseurl.upload}</MVN_RAWREPO_BASEURL_UPLOAD>
- <MVN_RAWREPO_BASEURL_DOWNLOAD>${onap.nexus.rawrepo.baseurl.download}</MVN_RAWREPO_BASEURL_DOWNLOAD>
- <MVN_RAWREPO_SERVERID>${onap.nexus.rawrepo.serverid}</MVN_RAWREPO_SERVERID>
- <MVN_DOCKERREGISTRY_SNAPSHOT>${onap.nexus.dockerregistry.snapshot}</MVN_DOCKERREGISTRY_SNAPSHOT>
- <MVN_DOCKERREGISTRY_RELEASE>${onap.nexus.dockerregistry.release}</MVN_DOCKERREGISTRY_RELEASE>
- <MVN_DOCKERREGISTRY_SNAPSHOT_SERVERID>${onap.nexus.dockerregistry.snapshot.serverid}</MVN_DOCKERREGISTRY_SNAPSHOT_SERVERID>
- <MVN_DOCKERREGISTRY_RELEASE_SERVERID>${onap.nexus.dockerregistry.release.serverid}</MVN_DOCKERREGISTRY_RELEASE_SERVERID>
- <MVN_PYPISERVER_BASEURL>${onap.nexus.pypiserver.baseurl}</MVN_PYPISERVER_BASEURL>
- <MVN_PYPISERVER_SERVERID>${onap.nexus.pypiserver.serverid}</MVN_PYPISERVER_SERVERID>
- </environmentVariables>
- </configuration>
- </plugin>
- </plugins>
- </pluginManagement>
- </build>
-
-</project>
diff --git a/app/uwsgi.ini b/app/uwsgi.ini
deleted file mode 100644
index f514897..0000000
--- a/app/uwsgi.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[uwsgi]
-module = app.main
-callable = app
diff --git a/app/app/config_binding_service/__init__.py b/config_binding_service/__init__.py
index 306a762..306a762 100644
--- a/app/app/config_binding_service/__init__.py
+++ b/config_binding_service/__init__.py
diff --git a/app/app/config_binding_service/client.py b/config_binding_service/client.py
index c6a6753..c6a6753 100644
--- a/app/app/config_binding_service/client.py
+++ b/config_binding_service/client.py
diff --git a/app/app/config_binding_service/controller.py b/config_binding_service/controller.py
index c2eb21c..c2eb21c 100644
--- a/app/app/config_binding_service/controller.py
+++ b/config_binding_service/controller.py
diff --git a/app/app/config_binding_service/logging.py b/config_binding_service/logging.py
index b6275a7..35750f2 100644
--- a/app/app/config_binding_service/logging.py
+++ b/config_binding_service/logging.py
@@ -1,5 +1,5 @@
# ============LICENSE_START=======================================================
-# Copyright (c) 2017-2018 AT&T Intellectual Property. All rights reserved.
+# Copyright (c) 2017-2019 AT&T Intellectual Property. All rights reserved.
# ================================================================================
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -16,15 +16,23 @@
#
# ECOMP is a trademark and service mark of AT&T Intellectual Property.
-from logging import getLogger, Formatter
+import logging
from logging.handlers import RotatingFileHandler
from os import makedirs
import datetime
+# These loggers will be overwritten with EELF logging when running in Docker
+_AUDIT_LOGGER = logging.getLogger("defaultlogger")
+_ERROR_LOGGER = logging.getLogger("defaultlogger")
+_METRICS_LOGGER = logging.getLogger("defaultlogger")
-_AUDIT_LOGGER = getLogger("defaultlogger")
-_ERROR_LOGGER = getLogger("defaultlogger")
-_METRICS_LOGGER = getLogger("defaultlogger")
+# Set up debug logger
+DEBUG_LOGGER = logging.getLogger("defaultlogger")
+handler = logging.StreamHandler()
+formatter = logging.Formatter("%(asctime)s [%(name)-12s] %(levelname)-8s %(message)s")
+handler.setFormatter(formatter)
+DEBUG_LOGGER.addHandler(handler)
+DEBUG_LOGGER.setLevel(logging.DEBUG)
def _create_logger(name, logfile):
@@ -33,16 +41,31 @@ def _create_logger(name, logfile):
https://docs.python.org/3/library/logging.handlers.html
what's with the non-pythonic naming in these stdlib methods? Shameful.
"""
- logger = getLogger(name)
- file_handler = RotatingFileHandler(logfile,
- maxBytes=10000000, backupCount=2) # 10 meg with one backup..
- formatter = Formatter('%(message)s')
+ logger = logging.getLogger(name)
+ file_handler = RotatingFileHandler(logfile, maxBytes=10000000, backupCount=2) # 10 meg with one backup..
+ formatter = logging.Formatter("%(message)s")
file_handler.setFormatter(formatter)
logger.setLevel("DEBUG")
logger.addHandler(file_handler)
return logger
+# Public
+
+
+def get_module_logger(mod_name):
+ """
+ To use this, do logger = get_module_logger(__name__)
+ """
+ logger = logging.getLogger(mod_name)
+ handler = logging.StreamHandler()
+ formatter = logging.Formatter("%(asctime)s [%(name)-12s] %(levelname)-8s %(message)s")
+ handler.setFormatter(formatter)
+ logger.addHandler(handler)
+ logger.setLevel(logging.DEBUG)
+ return logger
+
+
def create_loggers():
"""
Public method to set the global logger, launched from Run
@@ -52,19 +75,19 @@ def create_loggers():
# create the audit log
aud_file = "/opt/logs/audit.log"
- open(aud_file, 'a').close() # this is like "touch"
+ open(aud_file, "a").close() # this is like "touch"
global _AUDIT_LOGGER
_AUDIT_LOGGER = _create_logger("config_binding_service_audit", aud_file)
# create the error log
err_file = "/opt/logs/error.log"
- open(err_file, 'a').close() # this is like "touch"
+ open(err_file, "a").close() # this is like "touch"
global _ERROR_LOGGER
_ERROR_LOGGER = _create_logger("config_binding_service_error", err_file)
# create the metrics log
met_file = "/opt/logs/metrics.log"
- open(met_file, 'a').close() # this is like "touch"
+ open(met_file, "a").close() # this is like "touch"
global _METRICS_LOGGER
_METRICS_LOGGER = _create_logger("config_binding_service_metrics", met_file)
@@ -106,17 +129,21 @@ def audit(raw_request, bts, xer, rcode, calling_mod, msg="n/a"):
"""
ets = utc()
- _AUDIT_LOGGER.info("{bts}|{ets}|{xer}||n/a||{path}||{status}|{rcode}|||INFO||{servip}|{et}|config_binding_service|{clientip}|{calling_mod}|||||||{msg}".format(
- bts=bts.isoformat(),
- ets=ets.isoformat(),
- xer=xer, rcode=rcode,
- path=raw_request.path.split("/")[1],
- status="COMPLETE" if rcode < 400 else "ERROR",
- servip=raw_request.host.split(":")[0],
- et=int((ets - bts).microseconds / 1000), # supposed to be in milleseconds
- clientip=raw_request.remote_addr,
- calling_mod=calling_mod, msg=msg
- ))
+ _AUDIT_LOGGER.info(
+ "{bts}|{ets}|{xer}||n/a||{path}||{status}|{rcode}|||INFO||{servip}|{et}|config_binding_service|{clientip}|{calling_mod}|||||||{msg}".format(
+ bts=bts.isoformat(),
+ ets=ets.isoformat(),
+ xer=xer,
+ rcode=rcode,
+ path=raw_request.path.split("/")[1],
+ status="COMPLETE" if rcode < 400 else "ERROR",
+ servip=raw_request.host.split(":")[0],
+ et=int((ets - bts).microseconds / 1000), # supposed to be in milleseconds
+ clientip=raw_request.remote_addr,
+ calling_mod=calling_mod,
+ msg=msg,
+ )
+ )
def error(raw_request, xer, severity, ecode, tgt_entity="n/a", tgt_path="n/a", msg="n/a", adv_msg="n/a"):
@@ -140,16 +167,19 @@ def error(raw_request, xer, severity, ecode, tgt_entity="n/a", tgt_path="n/a", m
"""
ets = utc()
- _ERROR_LOGGER.error("{ets}|{xer}|n/a|{path}||{tge}|{tgp}|{sev}|{ecode}|{msg}|{amsg}".format(
- ets=ets,
- xer=xer,
- path=raw_request.path.split("/")[1],
- tge=tgt_entity,
- tgp=tgt_path,
- sev=severity,
- ecode=ecode,
- msg=msg,
- amsg=adv_msg))
+ _ERROR_LOGGER.error(
+ "{ets}|{xer}|n/a|{path}||{tge}|{tgp}|{sev}|{ecode}|{msg}|{amsg}".format(
+ ets=ets,
+ xer=xer,
+ path=raw_request.path.split("/")[1],
+ tge=tgt_entity,
+ tgp=tgt_path,
+ sev=severity,
+ ecode=ecode,
+ msg=msg,
+ amsg=adv_msg,
+ )
+ )
def metrics(raw_request, bts, xer, target, target_path, rcode, calling_mod, msg="n/a"):
@@ -188,17 +218,20 @@ def metrics(raw_request, bts, xer, target, target_path, rcode, calling_mod, msg=
"""
ets = utc()
- _METRICS_LOGGER.info("{bts}|{ets}|{xer}||n/a||{path}||{tge}|{tgp}|{status}|{rcode}|||INFO||{servip}|{et}|config_binding_service|{clientip}|{calling_mod}|||n/a|||||{msg}".format(
- bts=bts.isoformat(),
- ets=ets.isoformat(),
- xer=xer,
- path=raw_request.path.split("/")[1],
- tge=target,
- tgp=target_path,
- status="COMPLETE" if rcode < 400 else "ERROR",
- rcode=rcode,
- servip=raw_request.host.split(":")[0],
- et=int((ets - bts).microseconds / 1000), # supposed to be in milleseconds
- clientip=raw_request.remote_addr,
- calling_mod=calling_mod, msg=msg
- ))
+ _METRICS_LOGGER.info(
+ "{bts}|{ets}|{xer}||n/a||{path}||{tge}|{tgp}|{status}|{rcode}|||INFO||{servip}|{et}|config_binding_service|{clientip}|{calling_mod}|||n/a|||||{msg}".format(
+ bts=bts.isoformat(),
+ ets=ets.isoformat(),
+ xer=xer,
+ path=raw_request.path.split("/")[1],
+ tge=target,
+ tgp=target_path,
+ status="COMPLETE" if rcode < 400 else "ERROR",
+ rcode=rcode,
+ servip=raw_request.host.split(":")[0],
+ et=int((ets - bts).microseconds / 1000), # supposed to be in milleseconds
+ clientip=raw_request.remote_addr,
+ calling_mod=calling_mod,
+ msg=msg,
+ )
+ )
diff --git a/app/app/config_binding_service/openapi.yaml b/config_binding_service/openapi.yaml
index bc4dd49..96b19e4 100644
--- a/app/app/config_binding_service/openapi.yaml
+++ b/config_binding_service/openapi.yaml
@@ -1,6 +1,6 @@
openapi: 3.0.0
info:
- version: 2.3.0
+ version: 2.4.0
title: Config Binding Service
paths:
'/service_component/{service_component_name}':
diff --git a/app/app/main.py b/config_binding_service/run.py
index c7adaaf..175c0cf 100755
--- a/app/app/main.py
+++ b/config_binding_service/run.py
@@ -17,12 +17,16 @@
# ============LICENSE_END=========================================================
#
# ECOMP is a trademark and service mark of AT&T Intellectual Property.
-from config_binding_service.logging import create_loggers
+import os
+from gevent.pywsgi import WSGIServer
+from config_binding_service.logging import create_loggers, DEBUG_LOGGER
from config_binding_service import app
-if __name__ == "__main__":
- # Only for debugging while developing
- app.run(host='0.0.0.0', port=10000, debug=True)
-else:
- # Entrypoint in UWSGI
- create_loggers()
+
+def main():
+ """Entrypoint"""
+ if "PROD_LOGGING" in os.environ:
+ create_loggers()
+ DEBUG_LOGGER.debug("Starting gevent server")
+ http_server = WSGIServer(("", 10000), app)
+ http_server.serve_forever()
diff --git a/pom.xml b/pom.xml
index d5069b0..3ca5570 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,34 +20,111 @@ ECOMP is a trademark and service mark of AT&T Intellectual Property.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
+
<parent>
- <groupId>org.onap</groupId>
- <artifactId>dcaegen2</artifactId>
- <version>1.2.0-SNAPSHOT</version>
+ <groupId>org.onap.oparent</groupId>
+ <artifactId>oparent</artifactId>
+ <version>2.0.0</version>
</parent>
<!--- CHANGE THE FOLLOWING 3 OBJECTS for your own repo -->
<groupId>org.onap.dcaegen2.platform</groupId>
<artifactId>configbinding</artifactId>
<name>dcaegen2-platform-configbinding</name>
- <version>2.3.0-SNAPSHOT</version>
+ <version>2.4.0-SNAPSHOT</version>
<url>http://maven.apache.org</url>
-
- <packaging>pom</packaging>
- <modules>
- <module>app</module>
- </modules>
-
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- <!-- customize the SONARQUBE URL -->
- <sonar.host.url>http://localhost:9000</sonar.host.url>
- <!-- taken care of in the children -->
- <sonar.exclusions>**</sonar.exclusions>
+ <sonar.sources>.</sonar.sources>
+ <sonar.junit.reportsPath>xunit-results.xml</sonar.junit.reportsPath>
+ <sonar.python.coverage.reportPath>coverage.xml</sonar.python.coverage.reportPath>
+ <sonar.python.xunit.reportPath>xunit-results.xml</sonar.python.xunit.reportPath>
+ <sonar.language>py</sonar.language>
+ <sonar.pluginname>python</sonar.pluginname>
+ <sonar.inclusions>config_binding_service/*.py</sonar.inclusions>
+ <sonar.exclusions>tests/*,setup.py</sonar.exclusions>
</properties>
<build>
<finalName>${project.artifactId}-${project.version}</finalName>
<pluginManagement>
<plugins>
+ <!-- the following plugins are invoked from oparent, we do not need them -->
+ <plugin>
+ <groupId>org.sonatype.plugins</groupId>
+ <artifactId>nexus-staging-maven-plugin</artifactId>
+ <version>1.6.7</version>
+ <configuration>
+ <skipNexusStagingDeployMojo>true</skipNexusStagingDeployMojo>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <!-- This version supports the "deployAtEnd" parameter -->
+ <version>2.8</version>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-enforcer-plugin</artifactId>
+ <version>3.0.0-M1</version>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ <!-- first disable the default Java plugins at various stages -->
+ <!-- maven-resources-plugin is called during "*resource" phases by default behavior. it prepares the resources
+ dir. we do not need it -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-resources-plugin</artifactId>
+ <version>2.6</version>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ <!-- maven-compiler-plugin is called during "compile" phases by default behavior. we do not need it -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.1</version>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ <!-- maven-jar-plugin is called during "compile" phase by default behavior. we do not need it -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>2.4</version>
+ <executions>
+ <execution>
+ <id>default-jar</id>
+ <phase/>
+ </execution>
+ </executions>
+ </plugin>
+ <!-- maven-install-plugin is called during "install" phase by default behavior. it tries to copy stuff under
+ target dir to ~/.m2. we do not need it -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-install-plugin</artifactId>
+ <version>2.4</version>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ <!-- maven-surefire-plugin is called during "test" phase by default behavior. it triggers junit test.
+ we do not need it -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>2.12.4</version>
+ <configuration>
+ <skipTests>true</skipTests>
+ </configuration>
+ </plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
@@ -56,7 +133,6 @@ ECOMP is a trademark and service mark of AT&T Intellectual Property.
<executable>${session.executionRootDirectory}/mvn-phase-script.sh</executable>
<environmentVariables>
<!-- make mvn properties as env for our script -->
- <!-- make mvn properties as env for our script -->
<MVN_PROJECT_GROUPID>${project.groupId}</MVN_PROJECT_GROUPID>
<MVN_PROJECT_ARTIFACTID>${project.artifactId}</MVN_PROJECT_ARTIFACTID>
<MVN_PROJECT_VERSION>${project.version}</MVN_PROJECT_VERSION>
@@ -64,17 +140,131 @@ ECOMP is a trademark and service mark of AT&T Intellectual Property.
<MVN_RAWREPO_BASEURL_UPLOAD>${onap.nexus.rawrepo.baseurl.upload}</MVN_RAWREPO_BASEURL_UPLOAD>
<MVN_RAWREPO_BASEURL_DOWNLOAD>${onap.nexus.rawrepo.baseurl.download}</MVN_RAWREPO_BASEURL_DOWNLOAD>
<MVN_RAWREPO_SERVERID>${onap.nexus.rawrepo.serverid}</MVN_RAWREPO_SERVERID>
- <MVN_DOCKERREGISTRY_SNAPSHOT>${onap.nexus.dockerregistry.snapshot}</MVN_DOCKERREGISTRY_SNAPSHOT>
+ <MVN_DOCKERREGISTRY_DAILY>${onap.nexus.dockerregistry.daily}</MVN_DOCKERREGISTRY_DAILY>
<MVN_DOCKERREGISTRY_RELEASE>${onap.nexus.dockerregistry.release}</MVN_DOCKERREGISTRY_RELEASE>
- <MVN_DOCKERREGISTRY_SNAPSHOT_SERVERID>${onap.nexus.dockerregistry.snapshot.serverid}</MVN_DOCKERREGISTRY_SNAPSHOT_SERVERID>
- <MVN_DOCKERREGISTRY_RELEASE_SERVERID>${onap.nexus.dockerregistry.release.serverid}</MVN_DOCKERREGISTRY_RELEASE_SERVERID>
- <MVN_PYPISERVER_BASEURL>${onap.nexus.pypiserver.baseurl}</MVN_PYPISERVER_BASEURL>
- <MVN_PYPISERVER_SERVERID>${onap.nexus.pypiserver.serverid}</MVN_PYPISERVER_SERVERID>
</environmentVariables>
</configuration>
</plugin>
</plugins>
</pluginManagement>
+ <plugins>
+ <!-- plugin>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <version>2.4.1</version>
+ <configuration>
+ <descriptors>
+ <descriptor>assembly/dep.xml</descriptor>
+ </descriptors>
+ </configuration>
+ <executions>
+ <execution>
+ <id>make-assembly</id>
+ <phase>package</phase>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin -->
+ <!-- now we configure custom action (calling a script) at various lifecycle phases -->
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <version>1.2.1</version>
+ <executions>
+ <execution>
+ <id>clean phase script</id>
+ <phase>clean</phase>
+ <goals>
+ <goal>exec</goal>
+ </goals>
+ <configuration>
+ <arguments>
+ <argument>__</argument>
+ <argument>clean</argument>
+ </arguments>
+ </configuration>
+ </execution>
+ <execution>
+ <id>generate-sources script</id>
+ <phase>generate-sources</phase>
+ <goals>
+ <goal>exec</goal>
+ </goals>
+ <configuration>
+ <arguments>
+ <argument>__</argument>
+ <argument>generate-sources</argument>
+ </arguments>
+ </configuration>
+ </execution>
+ <execution>
+ <id>compile script</id>
+ <phase>compile</phase>
+ <goals>
+ <goal>exec</goal>
+ </goals>
+ <configuration>
+ <arguments>
+ <argument>__</argument>
+ <argument>compile</argument>
+ </arguments>
+ </configuration>
+ </execution>
+ <execution>
+ <id>package script</id>
+ <phase>package</phase>
+ <goals>
+ <goal>exec</goal>
+ </goals>
+ <configuration>
+ <arguments>
+ <argument>__</argument>
+ <argument>package</argument>
+ </arguments>
+ </configuration>
+ </execution>
+ <execution>
+ <id>test script</id>
+ <phase>test</phase>
+ <goals>
+ <goal>exec</goal>
+ </goals>
+ <configuration>
+ <arguments>
+ <argument>__</argument>
+ <argument>test</argument>
+ </arguments>
+ </configuration>
+ </execution>
+ <execution>
+ <id>install script</id>
+ <phase>install</phase>
+ <goals>
+ <goal>exec</goal>
+ </goals>
+ <configuration>
+ <arguments>
+ <argument>__</argument>
+ <argument>install</argument>
+ </arguments>
+ </configuration>
+ </execution>
+ <execution>
+ <id>deploy script</id>
+ <phase>deploy</phase>
+ <goals>
+ <goal>exec</goal>
+ </goals>
+ <configuration>
+ <arguments>
+ <argument>__</argument>
+ <argument>deploy</argument>
+ </arguments>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
</build>
-
</project>
diff --git a/app/app/setup.py b/setup.py
index 38929c9..5c71597 100644
--- a/app/app/setup.py
+++ b/setup.py
@@ -19,16 +19,14 @@
from setuptools import setup, find_packages
setup(
- name='config_binding_service',
- version='2.3.0',
+ name="config_binding_service",
+ version="2.4.0",
packages=find_packages(exclude=["tests.*", "tests"]),
author="Tommy Carpenter",
author_email="tommy@research.att.com",
- description='Service to fetch and bind configurations',
+ description="Service to fetch and bind configurations",
url="https://gerrit.onap.org/r/#/admin/projects/dcaegen2/platform/configbinding",
- install_requires=["requests",
- "Flask",
- "six",
- "connexion[swagger-ui]"],
- package_data={'config_binding_service': ['openapi.yaml']}
+ entry_points={"console_scripts": ["run.py=config_binding_service.run:main"]},
+ install_requires=["requests", "Flask", "six", "gevent", "connexion[swagger-ui]"],
+ package_data={"config_binding_service": ["openapi.yaml"]},
)
diff --git a/app/app/tests/__init__.py b/tests/__init__.py
index 1875bf6..1875bf6 100644
--- a/app/app/tests/__init__.py
+++ b/tests/__init__.py
diff --git a/app/app/tests/conftest.py b/tests/conftest.py
index c8f2a06..c8f2a06 100644
--- a/app/app/tests/conftest.py
+++ b/tests/conftest.py
diff --git a/app/app/tests/test_api.py b/tests/test_api.py
index 118a2a0..118a2a0 100644
--- a/app/app/tests/test_api.py
+++ b/tests/test_api.py
diff --git a/app/app/tests/test_client.py b/tests/test_client.py
index 96c3467..96c3467 100644
--- a/app/app/tests/test_client.py
+++ b/tests/test_client.py
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..a5342ca
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,43 @@
+# ============LICENSE_START=======================================================
+# Copyright (c) 2017-2019 AT&T Intellectual Property. All rights reserved.
+# ================================================================================
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ============LICENSE_END=========================================================
+#
+# ECOMP is a trademark and service mark of AT&T Intellectual Property.
+
+[tox]
+envlist = py36,flake8
+
+[testenv]
+deps=
+ pytest
+ coverage
+ pytest-cov
+setenv =
+ CONSUL_HOST = 8.8.8.8
+ HOSTNAME = config_binding_service
+ PYTHONPATH={toxinidir}
+commands=
+ pytest --junitxml xunit-results.xml --cov config_binding_service --cov-report xml --cov-report term --cov-fail-under=70
+ coverage xml -i
+
+[testenv:flake8]
+basepython = python3.6
+skip_install = true
+deps = flake8
+commands = flake8 setup.py config_binding_service tests
+
+[flake8]
+ignore = E501,W605
+
diff --git a/version.properties b/version.properties
index 8d40756..c0f75b6 100644
--- a/version.properties
+++ b/version.properties
@@ -1,5 +1,5 @@
major=2
-minor=3
+minor=4
patch=0
base_version=${major}.${minor}.${patch}
release_version=${base_version}