From e14b49ead38227ff17d760c4771d58d9c6d2e7c0 Mon Sep 17 00:00:00 2001 From: Tommy Carpenter Date: Wed, 29 May 2019 13:36:01 -0400 Subject: Switch to gevent Issue-ID: DCAEGEN2-1549 Change-Id: I762d9630f857a23b6ae61992d483cdca7bb6f88d Signed-off-by: Tommy Carpenter --- Changelog.md | 3 + Dockerfile | 28 +-- README.md | 76 ++----- app/app/config_binding_service/__init__.py | 46 ----- app/app/config_binding_service/client.py | 297 --------------------------- app/app/config_binding_service/controller.py | 108 ---------- app/app/config_binding_service/logging.py | 204 ------------------ app/app/config_binding_service/openapi.yaml | 112 ---------- app/app/main.py | 28 --- app/app/pom.xml | 270 ------------------------ app/app/setup.py | 34 --- app/app/tests/__init__.py | 21 -- app/app/tests/conftest.py | 70 ------- app/app/tests/test_api.py | 103 ---------- app/app/tests/test_client.py | 206 ------------------- app/app/tox.ini | 26 --- app/pom.xml | 80 -------- app/uwsgi.ini | 3 - config_binding_service/__init__.py | 46 +++++ config_binding_service/client.py | 297 +++++++++++++++++++++++++++ config_binding_service/controller.py | 108 ++++++++++ config_binding_service/logging.py | 237 +++++++++++++++++++++ config_binding_service/openapi.yaml | 112 ++++++++++ config_binding_service/run.py | 32 +++ pom.xml | 232 +++++++++++++++++++-- setup.py | 32 +++ tests/__init__.py | 21 ++ tests/conftest.py | 70 +++++++ tests/test_api.py | 103 ++++++++++ tests/test_client.py | 206 +++++++++++++++++++ tox.ini | 43 ++++ version.properties | 2 +- 32 files changed, 1543 insertions(+), 1713 deletions(-) delete mode 100644 app/app/config_binding_service/__init__.py delete mode 100644 app/app/config_binding_service/client.py delete mode 100644 app/app/config_binding_service/controller.py delete mode 100644 app/app/config_binding_service/logging.py delete mode 100644 app/app/config_binding_service/openapi.yaml delete mode 100755 app/app/main.py delete mode 100644 app/app/pom.xml delete mode 100644 app/app/setup.py delete mode 100644 app/app/tests/__init__.py delete mode 100644 app/app/tests/conftest.py delete mode 100644 app/app/tests/test_api.py delete mode 100644 app/app/tests/test_client.py delete mode 100644 app/app/tox.ini delete mode 100644 app/pom.xml delete mode 100644 app/uwsgi.ini create mode 100644 config_binding_service/__init__.py create mode 100644 config_binding_service/client.py create mode 100644 config_binding_service/controller.py create mode 100644 config_binding_service/logging.py create mode 100644 config_binding_service/openapi.yaml create mode 100755 config_binding_service/run.py create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_api.py create mode 100644 tests/test_client.py create mode 100644 tox.ini 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=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 ; ./main.py -``` + + pip install --ignore-installed .; set -x CONSUL_HOST ; ./run.py diff --git a/app/app/config_binding_service/__init__.py b/app/app/config_binding_service/__init__.py deleted file mode 100644 index 306a762..0000000 --- a/app/app/config_binding_service/__init__.py +++ /dev/null @@ -1,46 +0,0 @@ -# ============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. - -import os -import connexion - - -class BadEnviornmentENVNotFound(Exception): - """ - Specific exception to be raised when a required ENV varaible is missing - """ - pass - - -def get_consul_uri(): - """ - This method waterfalls reads an envioronmental variable called CONSUL_HOST - If that doesn't work, it raises an Exception - """ - if "CONSUL_HOST" in os.environ: - # WARNING! TODO! Currently the env file does not include the port. - # But some other people think that the port should be a part of that. - # For now, I'm hardcoding 8500 until this gets resolved. - return "http://{0}:{1}".format(os.environ["CONSUL_HOST"], 8500) - else: - raise BadEnviornmentENVNotFound("CONSUL_HOST") - - -# this has to be here due to circular dependency -app = connexion.App(__name__, specification_dir='.') -app.add_api('openapi.yaml', arguments={'title': 'Config Binding Service'}) diff --git a/app/app/config_binding_service/client.py b/app/app/config_binding_service/client.py deleted file mode 100644 index c6a6753..0000000 --- a/app/app/config_binding_service/client.py +++ /dev/null @@ -1,297 +0,0 @@ -# ============LICENSE_START======================================================= -# Copyright (c) 2017-2018 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. - -import re -from functools import partial, reduce -import base64 -import copy -import json -import requests -import six -from config_binding_service import get_consul_uri -from config_binding_service.logging import utc, metrics - - -CONSUL = get_consul_uri() - -template_match_rels = re.compile("\{{2}([^\}\{]*)\}{2}") -template_match_dmaap = re.compile("<{2}([^><]*)>{2}") - -### -# Cusom Exception -### - - -class CantGetConfig(Exception): - """ - Represents an exception where a required key in consul isn't there - """ - - def __init__(self, code, response): - self.code = code - self.response = response - - -class BadRequest(Exception): - """ - Exception to be raised when the user tried to do something they shouldn't - """ - - def __init__(self, response): - self.code = 400 - self.response = response - - -### -# Private Functions -### - - -def _consul_get_all_as_transaction(service_component_name, raw_request, xer): - """ - Use Consul's transaction API to get all keys of the form service_component_name:* - Return a dict with all the values decoded - """ - payload = [ - { - "KV": { - "Verb": "get-tree", - "Key": service_component_name, - } - }] - - bts = utc() - response = requests.put("{0}/v1/txn".format(CONSUL), json=payload) - metrics(raw_request, bts, xer, "Consul", "/v1/txn".format(service_component_name), response.status_code, __name__, msg="Retrieving Consul transaction for all keys for {0}".format(service_component_name)) - - try: - response.raise_for_status() - except requests.exceptions.HTTPError as exc: - raise CantGetConfig(exc.response.status_code, exc.response.text) - - result = json.loads(response.text)['Results'] - - new_res = {} - for res in result: - key = res["KV"]["Key"] - val = base64.b64decode(res["KV"]["Value"]).decode("utf-8") - try: - new_res[key] = json.loads(val) - except json.decoder.JSONDecodeError: - new_res[key] = "INVALID JSON" # TODO, should we just include the original value somehow? - - if service_component_name not in new_res: - raise CantGetConfig(404, "") - - return new_res - - -def _get_config_rels_dmaap(service_component_name, raw_request, xer): - allk = _consul_get_all_as_transaction(service_component_name, raw_request, xer) - config = allk[service_component_name] - rels = allk.get(service_component_name + ":rels", []) - dmaap = allk.get(service_component_name + ":dmaap", {}) - return config, rels, dmaap - - -def _get_connection_info_from_consul(service_component_name): - """ - Call consul's catalog - TODO: currently assumes there is only one service - - DEPRECATION NOTE: - This function existed when DCAE was using Consul to resolve service component's connection information. - This relied on a "rels" key and a Cloudify relationship plugin to set up the magic. - The consensous is that this feature is no longer used. - This functionality is very likely deprecated by Kubernetes service discovery mechanism, and DMaaP. - - This function also includes logic related to CDAP, which is also likely deprecated. - - This code shall remain here for now but is at risk of being deleted in a future release. - """ - # Note: there should be a metrics log here, but see the deprecation note above; this function is due to be deleted. - res = requests.get("{0}/v1/catalog/service/{1}".format(CONSUL, service_component_name)) - res.raise_for_status() - services = res.json() - if services == []: - return None # later will get filtered out - ip_addr = services[0]["ServiceAddress"] - port = services[0]["ServicePort"] - - if "cdap_app" in service_component_name: - redirectish_url = "http://{0}:{1}/application/{2}".format(ip_addr, port, service_component_name) - res = requests.get(redirectish_url) - res.raise_for_status() - details = res.json() - # Pick out the details to expose to the component developers. These keys come from the broker API - return {key: details[key] for key in ["connectionurl", "serviceendpoints"]} - return "{0}:{1}".format(ip_addr, port) - - -def _replace_rels_template(rels, template_identifier): - """ - The magic. Replaces a template identifier {{...}} with the entrie(s) from the rels keys - NOTE: There was a discussion over whether the CBS should treat {{}} as invalid. Mike asked that - it resolve to the empty list. So, it does resolve it to empty list. - """ - returnl = [] - for rel in rels: - if template_identifier in rel and template_identifier != "": - returnl.append(rel) - # returnl now contains a list of DNS names (possible empty), now resolve them (or not if they are not regustered) - return list(filter(lambda x: x is not None, map(_get_connection_info_from_consul, returnl))) - - -def _replace_dmaap_template(dmaap, template_identifier): - """ - This one liner could have been just put inline in the caller but maybe this will get more complex in future - Talked to Mike, default value if key is not found in dmaap key should be {} - """ - return {} if (template_identifier not in dmaap or template_identifier == "<<>>") else dmaap[template_identifier] - - -def _replace_value(v, rels, dmaap): - """ - Takes a value v that was some value in the templatized configuration, determines whether it needs replacement (either {{}} or <<>>), and if so, replaces it. - Otherwise just returns v - - implementation notes: - - the split below sees if we have v = x,y,z... so we can support {{x,y,z,....}} - - the lambda is because we can't fold operators in Python, wanted fold(+, L) where + when applied to lists in python is list concatenation - """ - if isinstance(v, six.string_types): # do not try to replace anything that is not a string - match_on_rels = re.match(template_match_rels, v) - if match_on_rels: - # now holds just x,.. of {{x,...}} - template_identifier = match_on_rels.groups()[0].strip() - rtpartial = partial(_replace_rels_template, rels) - return reduce(lambda a, b: a + b, map(rtpartial, template_identifier.split(",")), []) - match_on_dmaap = re.match(template_match_dmaap, v) - if match_on_dmaap: - template_identifier = match_on_dmaap.groups()[0].strip() - """ - Here is what Mike said: - 1) want simple replacement of "<< >>" with dmaap key value - 2) never need to support <> whereas we do support {{sct1,sct2}} - The consequence is that if you give the CBS a dmaap key like {"foo" : {...}} you are going to get back {...}, but rels always returns [...]. - So now component developers have to possible handle dicts and [], and we have to communicate that to them - """ - return _replace_dmaap_template(dmaap, template_identifier) - return v # was not a match or was not a string, return value as is - - -def _recurse(config, rels, dmaap): - """ - Recurse throug a configuration, or recursively a sub elemebt of it. - If it's a dict: recurse over all the values - If it's a list: recurse over all the values - If it's a string: return the replacement - If none of the above, just return the item. - """ - if isinstance(config, list): - return [_recurse(item, rels, dmaap) for item in config] - if isinstance(config, dict): - for key in config: - config[key] = _recurse(config[key], rels, dmaap) - return config - if isinstance(config, six.string_types): - return _replace_value(config, rels, dmaap) - # not a dict, not a list, not a string, nothing to do. - return config - - -######### -# PUBLIC API -######### - - -def resolve(service_component_name, raw_request, xer): - """ - Return the bound config of service_component_name - - raw_request and xer are needed to form the correct metrics log - """ - config, rels, dmaap = _get_config_rels_dmaap(service_component_name, raw_request, xer) - return _recurse(config, rels, dmaap) - - -def resolve_override(config, rels=[], dmaap={}): - """ - Explicitly take in a config, rels, dmaap and try to resolve it. - Useful for testing where you dont want to put the test values in consul - """ - # use deepcopy to make sure that config is not touched - return _recurse(copy.deepcopy(config), rels, dmaap) - - -def resolve_all(service_component_name, raw_request, xer): - """ - Return config, policies, and any other k such that service_component_name:k exists (other than :dmaap and :rels) - - raw_request and xer are needed to form the correct metrics log - """ - allk = _consul_get_all_as_transaction(service_component_name, raw_request, xer) - returnk = {} - - # replace the config with the resolved config - returnk["config"] = resolve_override(allk[service_component_name], - allk.get("{0}:rels".format(service_component_name), []), - allk.get("{0}:dmaap".format(service_component_name), {})) - - # concatenate the items - for k in allk: - if "policies" in k: - if "policies" not in returnk: - returnk["policies"] = {} - returnk["policies"]["event"] = {} - returnk["policies"]["items"] = [] - - if k.endswith(":policies/event"): - returnk["policies"]["event"] = allk[k] - elif ":policies/items" in k: - returnk["policies"]["items"].append(allk[k]) - else: - if not(k == service_component_name or k.endswith(":rels") or k.endswith(":dmaap")): - # this would blow up if you had a key in consul without a : but this shouldnt happen - suffix = k.split(":")[1] - returnk[suffix] = allk[k] - - return returnk - - -def get_key(key, service_component_name, raw_request, xer): - """ - Try to fetch a key k from Consul of the form service_component_name:k - - raw_request and xer are needed to form the correct metrics log - """ - if key == "policies": - raise BadRequest( - ":policies is a complex folder and should be retrieved using the service_component_all API") - - bts = utc() - path = "v1/kv/{0}:{1}".format(service_component_name, key) - response = requests.get("{0}/{1}".format(CONSUL, path)) - metrics(raw_request, bts, xer, "Consul", path, response.status_code, __name__, msg="Retrieving single Consul key {0} for {1}".format(key, service_component_name)) - - try: - response.raise_for_status() - except requests.exceptions.HTTPError as exc: - raise CantGetConfig(exc.response.status_code, exc.response.text) - rest = json.loads(response.text)[0] - return json.loads(base64.b64decode(rest["Value"]).decode("utf-8")) diff --git a/app/app/config_binding_service/controller.py b/app/app/config_binding_service/controller.py deleted file mode 100644 index c2eb21c..0000000 --- a/app/app/config_binding_service/controller.py +++ /dev/null @@ -1,108 +0,0 @@ -# ============LICENSE_START======================================================= -# Copyright (c) 2017-2018 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. - -import json -import requests -import connexion -import uuid -from flask import Response -from config_binding_service import client, get_consul_uri -from config_binding_service.logging import audit, utc, error, metrics - - -def _get_helper(json_expecting_func, **kwargs): - """ - Helper function used by several functions below - """ - try: - payload = json_expecting_func(**kwargs) - response, status_code, mimetype = json.dumps(payload), 200, "application/json" - except client.BadRequest as exc: - response, status_code, mimetype = exc.response, exc.code, "text/plain" - except client.CantGetConfig as exc: - response, status_code, mimetype = exc.response, exc.code, "text/plain" - except Exception: - response, status_code, mimetype = "Unknown error", 500, "text/plain" - return response, status_code, mimetype - - -def _get_or_generate_xer(raw_request): - """get or generate the transaction id""" - xer = raw_request.headers.get("x-onap-requestid", None) - if xer is None: - # some components are still using the old name - xer = raw_request.headers.get("x-ecomp-requestid", None) - if xer is None: - # the user did NOT supply a request id, generate one - xer = str(uuid.uuid4()) - return xer - - -def bind_all(service_component_name): - """ - Get all the keys in Consul for this SCN, and bind the config - """ - xer = _get_or_generate_xer(connexion.request) - bts = utc() - response, status_code, mimetype = _get_helper(client.resolve_all, service_component_name=service_component_name, raw_request=connexion.request, xer=xer) - audit(connexion.request, bts, xer, status_code, __name__, "called for component {0}".format(service_component_name)) - # Even though some older components might be using the ecomp name, we return the proper one - return Response(response=response, status=status_code, mimetype=mimetype, headers={"x-onap-requestid": xer}) - - -def bind_config_for_scn(service_component_name): - """ - Bind just the config for this SCN - """ - xer = _get_or_generate_xer(connexion.request) - bts = utc() - response, status_code, mimetype = _get_helper(client.resolve, service_component_name=service_component_name, raw_request=connexion.request, xer=xer) - audit(connexion.request, bts, xer, status_code, __name__, "called for component {0}".format(service_component_name)) - return Response(response=response, status=status_code, mimetype=mimetype, headers={"x-onap-requestid": xer}) - - -def get_key(key, service_component_name): - """ - Get a single key k of the form service_component_name:k from Consul. - Should not be used and will return a BAD REQUEST for k=policies because it's a complex object - """ - xer = _get_or_generate_xer(connexion.request) - bts = utc() - response, status_code, mimetype = _get_helper(client.get_key, key=key, service_component_name=service_component_name, raw_request=connexion.request, xer=xer) - audit(connexion.request, bts, xer, status_code, __name__, "called for component {0}".format(service_component_name)) - return Response(response=response, status=status_code, mimetype=mimetype, headers={"x-onap-requestid": xer}) - - -def healthcheck(): - """ - CBS Healthcheck - """ - xer = _get_or_generate_xer(connexion.request) - path = "v1/catalog/service/config_binding_service" - bts = utc() - res = requests.get("{0}/{1}".format(get_consul_uri(), path)) - status = res.status_code - if status == 200: - msg = "CBS is alive and Consul connection OK" - else: - msg = "CBS is alive but cannot reach Consul" - # treating this as a WARN because this could be a temporary network glitch. Also per EELF guidelines this is a 200 ecode (availability) - error(connexion.request, xer, "WARN", 200, tgt_entity="Consul", tgt_path="/v1/catalog/service/config_binding_service", msg=msg) - metrics(connexion.request, bts, xer, "Consul", path, res.status_code, __name__, msg="Checking Consul connectivity during CBS healthcheck, {0}".format(msg)) - audit(connexion.request, bts, xer, status, __name__, msg=msg) - return Response(response=msg, status=status) diff --git a/app/app/config_binding_service/logging.py b/app/app/config_binding_service/logging.py deleted file mode 100644 index b6275a7..0000000 --- a/app/app/config_binding_service/logging.py +++ /dev/null @@ -1,204 +0,0 @@ -# ============LICENSE_START======================================================= -# Copyright (c) 2017-2018 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. - -from logging import getLogger, Formatter -from logging.handlers import RotatingFileHandler -from os import makedirs -import datetime - - -_AUDIT_LOGGER = getLogger("defaultlogger") -_ERROR_LOGGER = getLogger("defaultlogger") -_METRICS_LOGGER = getLogger("defaultlogger") - - -def _create_logger(name, logfile): - """ - Create a RotatingFileHandler and a streamhandler for stdout - 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') - file_handler.setFormatter(formatter) - logger.setLevel("DEBUG") - logger.addHandler(file_handler) - return logger - - -def create_loggers(): - """ - Public method to set the global logger, launched from Run - This is *not* launched during unit testing, so unit tests do not create/write log files - """ - makedirs("/opt/logs", exist_ok=True) - - # create the audit log - aud_file = "/opt/logs/audit.log" - 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" - 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" - global _METRICS_LOGGER - _METRICS_LOGGER = _create_logger("config_binding_service_metrics", met_file) - - -def utc(): - """gets current time in utc""" - return datetime.datetime.utcnow() - - -def audit(raw_request, bts, xer, rcode, calling_mod, msg="n/a"): - """ - write an EELF audit record per https://wiki.onap.org/download/attachments/1015849/ONAP%20application%20logging%20guidelines.pdf?api=v2 - %The audit fields implemented: - - 1 BeginTimestamp Implemented (bts) - 2 EndTimestamp Auto Injected when this is called - 3 RequestID Implemented (xer) - 5 threadId n/a - 7 serviceName Implemented (from Req) - 9 StatusCode Auto injected based on rcode - 10 ResponseCode Implemented (rcode) - 13 Category log level - all audit records are INFO. - 15 Server IP address Implemented (from Req) - 16 ElapsedTime Auto Injected (milliseconds) - 17 Server This is running in a Docker container so this is not applicable, my HOSTNAME is always "config_binding_service" - 18 ClientIPaddress Implemented (from Req) - 19 class name Implemented (mod), though docs say OOP, I am using the python module here - 20 Unused ...implemented.... - 21-25 Custom n/a - 26 detailMessage Implemented (msg) - - Not implemented - 4 serviceInstanceID - ? - 6 physical/virtual server name (Optional) - 8 PartnerName - nothing in the request tells me this - 11 Response Description - the CBS follows standard HTTP error codes so look them up - 12 instanceUUID - Optional - 14 Severity (Optional) - """ - 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 - )) - - -def error(raw_request, xer, severity, ecode, tgt_entity="n/a", tgt_path="n/a", msg="n/a", adv_msg="n/a"): - """ - write an EELF error record per - the error fields implemented: - - 1 Timestamp Auto Injected when this is called - 2 RequestID Implemented (xer) - 3 ThreadID n/a - 4 ServiceName Implemented (from Req) - 6 TargetEntity Implemented (tgt_entity) - 7 TargetServiceName Implemented (tgt_path)/ - 8 ErrorCategory Implemented (severity) - 9. ErrorCode Implemented (ecode) - 10 ErrorDescription Implemented (msg) - 11. detailMessage Implemented (adv_msg) - - Not implemented: - 5 PartnerName - nothing in the request tells me this - """ - 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)) - - -def metrics(raw_request, bts, xer, target, target_path, rcode, calling_mod, msg="n/a"): - """ - write an EELF metrics record per https://wiki.onap.org/download/attachments/1015849/ONAP%20application%20logging%20guidelines.pdf?api=v2 - %The metrics fields implemented: - - 1 BeginTimestamp Implemented (bts) - 2 EndTimestamp Auto Injected when this is called - 3 RequestID Implemented (xer) - 5 threadId n/a - 7 serviceName Implemented (from Req) - 9 TargetEntity Implemented (target) - 10 TargetServiceName Implemented (target_path) - 11 StatusCode Implemented (based on rcode) - 12 Response Code Implemented (rcode) - 15 Category log level all metrics records are INFO. - 17 Server IP address Implemented (from Req) - 18 ElapsedTime Auto Injected (milliseconds) - 19 Server This is running in a Docker container so this is not applicable, my HOSTNAME is always "config_binding_service" - 20 ClientIPaddress Implemented (from Req) - 21 class name Implemented (mod), though docs say OOP, I am using the python module here - 22 Unused ...implemented.... - 24 TargetVirtualEntity n/a - 25-28 Custom n/a - 29 detailMessage Implemented (msg) - - Not implemented - 4 serviceInstanceID - ? - 6 physical/virtual server name (Optional) - 8 PartnerName - nothing in the request tells me this - 13 Response Description - the CBS follows standard HTTP error codes so look them up - 14 instanceUUID - Optional - 16 Severity (Optional) - 23 ProcessKey - optional - """ - 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 - )) diff --git a/app/app/config_binding_service/openapi.yaml b/app/app/config_binding_service/openapi.yaml deleted file mode 100644 index bc4dd49..0000000 --- a/app/app/config_binding_service/openapi.yaml +++ /dev/null @@ -1,112 +0,0 @@ -openapi: 3.0.0 -info: - version: 2.3.0 - title: Config Binding Service -paths: - '/service_component/{service_component_name}': - parameters: - - name: service_component_name - in: path - description: >- - Service Component Name. service_component_name must be a key in - consul. - required: true - schema: - type: string - get: - description: >- - Binds the configuration for service_component_name and returns the bound - configuration as a JSON - operationId: config_binding_service.controller.bind_config_for_scn - responses: - '200': - description: OK; the bound config is returned as an object - content: - '*/*': - schema: - type: object - '404': - description: there is no configuration in Consul for this component - '/service_component_all/{service_component_name}': - parameters: - - name: service_component_name - in: path - description: >- - Service Component Name. service_component_name must be a key in - consul. - required: true - schema: - type: string - get: - description: >- - Binds the configuration for service_component_name and returns the bound - configuration, policies, and any other keys that are in Consul - operationId: config_binding_service.controller.bind_all - responses: - '200': - description: >- - OK; returns {config : ..., policies : ....., k : ...} for all other - k in Consul - content: - '*/*': - schema: - type: object - '404': - description: there is no configuration in Consul for this component - '/{key}/{service_component_name}': - parameters: - - name: key - in: path - description: >- - this endpoint tries to pull service_component_name:key; key is the key - after the colon - required: true - schema: - type: string - - name: service_component_name - in: path - description: Service Component Name. - required: true - schema: - type: string - get: - description: >- - this is an endpoint that fetches a generic service_component_name:key - out of Consul. The idea is that we don't want to tie components to - Consul directly in case we swap out the backend some day, so the CBS - abstracts Consul from clients. The structuring and weird collision of - this new API with the above is unfortunate but due to legacy concerns. - operationId: config_binding_service.controller.get_key - responses: - '200': - description: 'OK; returns service_component_name:key' - content: - '*/*': - schema: - type: object - '400': - description: >- - bad request. Currently this is only returned on :policies, which is - a complex object, and should be gotten through service_component_all - content: - '*/*': - schema: - type: string - '404': - description: key does not exist - content: - '*/*': - schema: - type: string - /healthcheck: - get: - description: >- - This is the health check endpoint. If this returns a 200, the server is - alive and consul can be reached. If not a 200, either dead, or no - connection to consul - operationId: config_binding_service.controller.healthcheck - responses: - '200': - description: Successful response - '503': - description: the config binding service cannot reach Consul diff --git a/app/app/main.py b/app/app/main.py deleted file mode 100755 index c7adaaf..0000000 --- a/app/app/main.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 - -# ============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. -from config_binding_service.logging import create_loggers -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() 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 @@ - - - - 4.0.0 - - - org.onap.dcaegen2.platform.configbinding - app - 2.3.0-SNAPSHOT - - - org.onap.dcaegen2.platform.configbinding - app-app - dcaegen2-platform-configbinding-app-app - 2.3.0-SNAPSHOT - http://maven.apache.org - - UTF-8 - . - xunit-results.xml - coverage.xml - xunit-results.xml - py - python - config_binding_service/*.py - tests/*,setup.py - - - ${project.artifactId}-${project.version} - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.7 - - true - - - - org.apache.maven.plugins - maven-deploy-plugin - - 2.8 - - true - - - - org.apache.maven.plugins - maven-enforcer-plugin - 3.0.0-M1 - - true - - - - - - org.apache.maven.plugins - maven-resources-plugin - 2.6 - - true - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.1 - - true - - - - - org.apache.maven.plugins - maven-jar-plugin - 2.4 - - - default-jar - - - - - - - org.apache.maven.plugins - maven-install-plugin - 2.4 - - true - - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.12.4 - - true - - - - org.codehaus.mojo - exec-maven-plugin - 1.2.1 - - ${session.executionRootDirectory}/mvn-phase-script.sh - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${onap.nexus.url} - ${onap.nexus.rawrepo.baseurl.upload} - ${onap.nexus.rawrepo.baseurl.download} - ${onap.nexus.rawrepo.serverid} - ${onap.nexus.dockerregistry.daily} - ${onap.nexus.dockerregistry.release} - - - - - - - - - - org.codehaus.mojo - exec-maven-plugin - 1.2.1 - - - clean phase script - clean - - exec - - - - __ - clean - - - - - generate-sources script - generate-sources - - exec - - - - __ - generate-sources - - - - - compile script - compile - - exec - - - - __ - compile - - - - - package script - package - - exec - - - - __ - package - - - - - test script - test - - exec - - - - __ - test - - - - - install script - install - - exec - - - - __ - install - - - - - deploy script - deploy - - exec - - - - __ - deploy - - - - - - - - diff --git a/app/app/setup.py b/app/app/setup.py deleted file mode 100644 index 38929c9..0000000 --- a/app/app/setup.py +++ /dev/null @@ -1,34 +0,0 @@ -# ============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. - -from setuptools import setup, find_packages - -setup( - name='config_binding_service', - version='2.3.0', - packages=find_packages(exclude=["tests.*", "tests"]), - author="Tommy Carpenter", - author_email="tommy@research.att.com", - 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']} -) diff --git a/app/app/tests/__init__.py b/app/app/tests/__init__.py deleted file mode 100644 index 1875bf6..0000000 --- a/app/app/tests/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# ================================================================================ -# Copyright (c) 2018 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. - - -# empty __init__.py so that pytest can add correct path to coverage report, -- per pytest -# best practice guideline diff --git a/app/app/tests/conftest.py b/app/app/tests/conftest.py deleted file mode 100644 index c8f2a06..0000000 --- a/app/app/tests/conftest.py +++ /dev/null @@ -1,70 +0,0 @@ -import pytest -from requests.exceptions import HTTPError -from config_binding_service import get_consul_uri - - -class FakeResponse(): - def __init__(self, status_code, text): - self.text = text - self.status_code = status_code - - def raise_for_status(self): - if self.status_code >= 400: - raise HTTPError(response=FakeResponse(404, "")) - - -@pytest.fixture -def expected_config(): - return {"deep": {"ALL YOUR SERVICE BELONG TO US": ['6.6.6.6:666', '7.7.7.7:777', '5.5.5.5:555', '5.5.5.5:555']}, - "doubledeep": {"sodeep": {"hello": "darkness"}}} - - -@pytest.fixture -def monkeyed_get_connection_info_from_consul(): - def _monkeyed_get_connection_info_from_consul(service_component_name): - # shared monkeypatch. probably somewhat lazy because the function htis patches can be broken up. - if service_component_name == "cdap": - return '666.666.666.666:666' - elif service_component_name == "testing_bravo.somedomain.com": - return '7.7.7.7:777' - elif service_component_name == "testing_alpha.somedomain.com": - return '6.6.6.6:666' - elif service_component_name == "testing_charlie.somedomain.com": - return '5.5.5.5:555' - elif service_component_name == "nonexistent_hope": - return None # the real function returns None here - elif service_component_name == "cdap_serv.dcae.ecomp.somedomain.com": - broker_ip = '1.1.1.1' - broker_port = 444 - return "http://{0}:{1}/application/{2}".format(broker_ip, broker_port, service_component_name) - return _monkeyed_get_connection_info_from_consul - - -@pytest.fixture -def monkeyed_requests_get(): - def _monkeyed_requests_get(url): - if url == "{0}/v1/kv/test_service_component_name.unknown.unknown.unknown.dcae.onap.org:dti".format(get_consul_uri()): - return FakeResponse(status_code=200, text='[{"LockIndex":0,"Key":"test_service_component_name.unknown.unknown.unknown.dcae.onap.org:dti","Flags":0,"Value": "eyJteSIgOiAiZHRpIn0=","CreateIndex":4066524,"ModifyIndex":4066524}]') - else: - return FakeResponse(status_code=404, text="") - return _monkeyed_requests_get - - -@pytest.fixture -def monkeyed_requests_put(): - def _monkeyed_requests_put(url, json): - if url == "{0}/v1/txn".format(get_consul_uri()): - key = json[0]["KV"]["Key"] - if key == "test_service_component_name.unknown.unknown.unknown.dcae.onap.org": - return FakeResponse(status_code=200, text='{"Results":[{"KV":{"LockIndex":0,"Key":"test_service_component_name.unknown.unknown.unknown.dcae.onap.org","Flags":0,"Value":"eyJteSIgOiAiYW1hemluZyBjb25maWcifQ==","CreateIndex":4051555,"ModifyIndex":4051555}},{"KV":{"LockIndex":0,"Key":"test_service_component_name.unknown.unknown.unknown.dcae.onap.org:dmaap","Flags":0,"Value":"eyJmb28iIDogImJhciJ9","CreateIndex":4051571,"ModifyIndex":4051571}},{"KV":{"LockIndex":0,"Key":"test_service_component_name.unknown.unknown.unknown.dcae.onap.org:dti","Flags":0,"Value":"eyJteSIgOiAiZHRpIn0=","CreateIndex":4066524,"ModifyIndex":4066524}},{"KV":{"LockIndex":0,"Key":"test_service_component_name.unknown.unknown.unknown.dcae.onap.org:policies/event","Flags":0,"Value":"eyJhY3Rpb24iOiAiZ2F0aGVyZWQiLCAidGltZXN0YW1wIjogIjIwMTgtMDItMTkgMTU6MzY6NDQuODc3MzgwIiwgInVwZGF0ZV9pZCI6ICJiYjczYzIwYS01ZmY4LTQ1MGYtODIyMy1kYTY3MjBhZGUyNjciLCAicG9saWNpZXNfY291bnQiOiAyfQ==","CreateIndex":4048564,"ModifyIndex":4048564}},{"KV":{"LockIndex":0,"Key":"test_service_component_name.unknown.unknown.unknown.dcae.onap.org:policies/items/DCAE_alex.Config_MS_alex_microservice","Flags":0,"Value":"eyJwb2xpY3lOYW1lIjogIkRDQUVfYWxleC5Db25maWdfTVNfYWxleF9taWNyb3NlcnZpY2UuMTMyLnhtbCIsICJwb2xpY3lDb25maWdNZXNzYWdlIjogIkNvbmZpZyBSZXRyaWV2ZWQhICIsICJyZXNwb25zZUF0dHJpYnV0ZXMiOiB7fSwgInBvbGljeUNvbmZpZ1N0YXR1cyI6ICJDT05GSUdfUkVUUklFVkVEIiwgIm1hdGNoaW5nQ29uZGl0aW9ucyI6IHsiT05BUE5hbWUiOiAiRENBRSIsICJOYW1lIjogIkRDQUUiLCAiQ29uZmlnTmFtZSI6ICJhbGV4X2NvbmZpZ19uYW1lIn0sICJjb25maWciOiB7InBvbGljeVNjb3BlIjogImFsZXhfcG9saWN5X3Njb3BlIiwgImNvbmZpZ05hbWUiOiAiYWxleF9jb25maWdfbmFtZSIsICJkZXNjcmlwdGlvbiI6ICJ0ZXN0IERDQUUgcG9saWN5LWhhbmRsZXIiLCAic2VydmljZSI6ICJhbGV4X3NlcnZpY2UiLCAicG9saWN5TmFtZSI6ICJhbGV4X3BvbGljeV9uYW1lIiwgInJpc2tMZXZlbCI6ICIzIiwgImtleTEiOiAidmFsdWUxIiwgInBvbGljeV9oZWxsbyI6ICJ3b3JsZCEiLCAiY29udGVudCI6IHsiZm9vIjogIm1pY3Jvc2VydmljZTMiLCAiZm9vX3VwZGF0ZWQiOiAiMjAxOC0wMS0zMFQxMzoyNTozMy4yMjJaIn0sICJyaXNrVHlwZSI6ICIxNzEyX0VURSIsICJndWFyZCI6ICJGYWxzZSIsICJ2ZXJzaW9uIjogIjAuMC4xIiwgImxvY2F0aW9uIjogIkNlbnRyYWwiLCAicG9saWN5X3VwZGF0ZWRfdHMiOiAiMjAxOC0wMi0xOVQxNTowOTo1NS4yMTdaIiwgInVwZGF0ZWRfcG9saWN5X2lkIjogIkRDQUVfYWxleC5Db25maWdfTVNfYWxleF9taWNyb3NlcnZpY2UiLCAicG9saWN5X3VwZGF0ZWRfdG9fdmVyIjogIjEzMiIsICJwcmlvcml0eSI6ICI0IiwgInBvbGljeV91cGRhdGVkX2Zyb21fdmVyIjogIjEzMSIsICJ0ZW1wbGF0ZVZlcnNpb24iOiAiMiIsICJ1dWlkIjogIjVlODdkN2M1LTBkYWYtNGI2Yi1hYjkyLTUzNjVjZjVkYjFlZiJ9LCAicHJvcGVydHkiOiBudWxsLCAidHlwZSI6ICJKU09OIiwgInBvbGljeVZlcnNpb24iOiAiMTMyIn0=","CreateIndex":4048564,"ModifyIndex":4065574}},{"KV":{"LockIndex":0,"Key":"test_service_component_name.unknown.unknown.unknown.dcae.onap.org:policies/items/DCAE_alex.Config_db_client_policy_id_value","Flags":0,"Value":"eyJwb2xpY3lOYW1lIjogIkRDQUVfYWxleC5Db25maWdfZGJfY2xpZW50X3BvbGljeV9pZF92YWx1ZS4xMzMueG1sIiwgInBvbGljeUNvbmZpZ01lc3NhZ2UiOiAiQ29uZmlnIFJldHJpZXZlZCEgIiwgInJlc3BvbnNlQXR0cmlidXRlcyI6IHt9LCAicG9saWN5Q29uZmlnU3RhdHVzIjogIkNPTkZJR19SRVRSSUVWRUQiLCAibWF0Y2hpbmdDb25kaXRpb25zIjogeyJPTkFQTmFtZSI6ICJEQ0FFIiwgIk5hbWUiOiAiRENBRSIsICJDb25maWdOYW1lIjogImFsZXhfY29uZmlnX25hbWUifSwgImNvbmZpZyI6IHsiZGJfY2xpZW50X3RzIjogIjIwMTctMTEtMjFUMTI6MTI6MTMuNjk2WiIsICJkYl9jbGllbnQiOiAiaXBzdW0iLCAicG9saWN5X2hlbGxvIjogIndvcmxkISIsICJwb2xpY3lfdXBkYXRlZF9mcm9tX3ZlciI6ICIxMzIiLCAidXBkYXRlZF9wb2xpY3lfaWQiOiAiRENBRV9hbGV4LkNvbmZpZ19kYl9jbGllbnRfcG9saWN5X2lkX3ZhbHVlIiwgInBvbGljeV91cGRhdGVkX3RzIjogIjIwMTgtMDItMTlUMTU6MDk6NTUuODEyWiIsICJwb2xpY3lfdXBkYXRlZF90b192ZXIiOiAiMTMzIn0sICJwcm9wZXJ0eSI6IG51bGwsICJ0eXBlIjogIkpTT04iLCAicG9saWN5VmVyc2lvbiI6ICIxMzMifQ==","CreateIndex":4048564,"ModifyIndex":4065570}},{"KV":{"LockIndex":0,"Key":"test_service_component_name.unknown.unknown.unknown.dcae.onap.org:rels","Flags":0,"Value":"WyJteS5hbWF6aW5nLnJlbGF0aW9uc2hpcCJd","CreateIndex":4051567,"ModifyIndex":4051567}}],"Errors":null,"Index":0,"LastContact":0,"KnownLeader":true}') - elif key == "scn_exists": - return FakeResponse(status_code=200, text='{"Results":[{"KV":{"LockIndex":0,"Key":"scn_exists","Flags":0,"Value":"eyJmb28zIiA6ICJiYXIzIn0=","CreateIndex":4067403,"ModifyIndex":4067403}},{"KV":{"LockIndex":0,"Key":"scn_exists:dmaap","Flags":0,"Value":"eyJmb280IiA6ICJiYXI0In0=","CreateIndex":4067410,"ModifyIndex":4067410}},{"KV":{"LockIndex":0,"Key":"scn_exists:rels","Flags":0,"Value":"WyJmb28iXQ==","CreateIndex":4067406,"ModifyIndex":4067406}},{"KV":{"LockIndex":0,"Key":"scn_exists_nord","Flags":0,"Value":"eyJmb281IiA6ICJiYXI1In0=","CreateIndex":4067340,"ModifyIndex":4067340}}],"Errors":null,"Index":0,"LastContact":0,"KnownLeader":true}') - elif key == "scn_exists_nord": - return FakeResponse(status_code=200, text='{"Results":[{"KV":{"LockIndex":0,"Key":"scn_exists_nord","Flags":0,"Value":"eyJmb281IiA6ICJiYXI1In0=","CreateIndex":4067340,"ModifyIndex":4067340}}],"Errors":null,"Index":0,"LastContact":0,"KnownLeader":true}') - elif key == "test_resolve_scn": - return FakeResponse(status_code=200, text='{"Results":[{"KV":{"LockIndex":0,"Key":"test_resolve_scn","Flags":0,"Value":"ewogICAgICAgICAgICAgICAgImRlZXAiIDogewogICAgICAgICAgICAgICAgICAgICJBTEwgWU9VUiBTRVJWSUNFIEJFTE9ORyBUTyBVUyIgOiAie3thbHBoYSxicmF2byxjaGFybGllfX0ifSwKICAgICAgICAgICAgICAgICJkb3VibGVkZWVwIiA6ICB7CiAgICAgICAgICAgICAgICAgICAgInNvZGVlcCIgOiB7ImhlbGxvIiA6ICI8PFdITz8+PiJ9fQogICAgICAgICAgICAgfQo=","CreateIndex":4068002,"ModifyIndex":4068002}},{"KV":{"LockIndex":0,"Key":"test_resolve_scn:dmaap","Flags":0,"Value":"eyJXSE8/IiA6ICJkYXJrbmVzcyJ9","CreateIndex":4068013,"ModifyIndex":4068013}},{"KV":{"LockIndex":0,"Key":"test_resolve_scn:rels","Flags":0,"Value":"WyJ0ZXN0aW5nX2FscGhhLnNvbWVkb21haW4uY29tIiwgInRlc3RpbmdfYnJhdm8uc29tZWRvbWFpbi5jb20iLCAidGVzdGluZ19jaGFybGllLnNvbWVkb21haW4uY29tIiwgInRlc3RpbmdfY2hhcmxpZS5zb21lZG9tYWluLmNvbSJd","CreateIndex":4068010,"ModifyIndex":4068010}}],"Errors":null,"Index":0,"LastContact":0,"KnownLeader":true}') - elif key == "cbs_test_messed_up": - return FakeResponse(status_code=200, text='{"Results":[{"KV":{"LockIndex":0,"Key":"cbs_test_messed_up","Flags":0,"Value":"eyJmb28iIDogImJhciJ9","CreateIndex":4864032,"ModifyIndex":4864052}},{"KV":{"LockIndex":0,"Key":"cbs_test_messed_up:badkey","Flags":0,"Value":"eyJub3QgYSBqc29ubm5uIFJFS1RUVFQ=","CreateIndex":4864075,"ModifyIndex":4864075}}],"Errors":null,"Index":0,"LastContact":0,"KnownLeader":true}') - elif key == "scn_NOTexists": - return FakeResponse(status_code=404, text="") - return _monkeyed_requests_put diff --git a/app/app/tests/test_api.py b/app/app/tests/test_api.py deleted file mode 100644 index 118a2a0..0000000 --- a/app/app/tests/test_api.py +++ /dev/null @@ -1,103 +0,0 @@ -# ============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. -import json -import pytest -import os -import tempfile -from config_binding_service import app - - -TEST_NAME = "test_service_component_name.unknown.unknown.unknown.dcae.onap.org" - - -# http://flask.pocoo.org/docs/1.0/testing/ -@pytest.fixture -def cbsclient(): - db_fd, app.app.config['DATABASE'] = tempfile.mkstemp() - app.app.config['TESTING'] = True - testclient = app.app.test_client() - - yield testclient - - os.close(db_fd) - os.unlink(app.app.config['DATABASE']) - - -def test_get(monkeypatch, cbsclient, monkeyed_requests_put): - monkeypatch.setattr('requests.put', monkeyed_requests_put) - - res = cbsclient.get('/service_component/scn_exists', - headers={"x-onap-requestid": 123456789}) - assert(json.loads(res.data) == {"foo3": "bar3"}) - assert(res.status_code == 200) - assert(res.headers["x-onap-requestid"] == "123456789") - - res = cbsclient.get('/service_component/scn_NOTexists', - headers={"x-onap-requestid": 123456789}) - assert(res.status_code == 404) - assert(res.headers["x-onap-requestid"] == "123456789") - - res = cbsclient.get('/service_component/asdfasdf', - headers={"x-onap-requestid": 123456789}) - assert(res.status_code == 500) - assert(res.headers["x-onap-requestid"] == "123456789") - - -def test_generic(monkeypatch, cbsclient, monkeyed_requests_get, monkeyed_requests_put): - monkeypatch.setattr('requests.put', monkeyed_requests_put) - monkeypatch.setattr('requests.get', monkeyed_requests_get) - - res = cbsclient.get('/dti/{0}'.format(TEST_NAME)) - assert json.loads(res.data) == {"my": "dti"} - assert res.json == {"my": "dti"} - assert res.status_code == 200 - assert "x-onap-requestid" in res.headers - - res = cbsclient.get('/nokeyforyou/{0}'.format(TEST_NAME)) - assert res.status_code == 404 - assert "x-onap-requestid" in res.headers - - res = cbsclient.get('/policies/{0}'.format(TEST_NAME)) - assert res.status_code == 400 - assert "x-onap-requestid" in res.headers - - -def test_resolve_all(monkeypatch, cbsclient, monkeyed_requests_put, monkeyed_get_connection_info_from_consul, expected_config): - monkeypatch.setattr('requests.put', monkeyed_requests_put) - monkeypatch.setattr('config_binding_service.client._get_connection_info_from_consul', monkeyed_get_connection_info_from_consul) - withstuff = {'config': {'my': 'amazing config'}, - 'dti': {'my': 'dti'}, - 'policies': {'items': [{'policyName': 'DCAE_alex.Config_MS_alex_microservice.132.xml', 'policyConfigMessage': 'Config Retrieved! ', 'responseAttributes': {}, 'policyConfigStatus': 'CONFIG_RETRIEVED', 'matchingConditions': {'ONAPName': 'DCAE', 'Name': 'DCAE', 'ConfigName': 'alex_config_name'}, 'config': {'policyScope': 'alex_policy_scope', 'configName': 'alex_config_name', 'description': 'test DCAE policy-handler', 'service': 'alex_service', 'policyName': 'alex_policy_name', 'riskLevel': '3', 'key1': 'value1', 'policy_hello': 'world!', 'content': {'foo': 'microservice3', 'foo_updated': '2018-01-30T13:25:33.222Z'}, 'riskType': '1712_ETE', 'guard': 'False', 'version': '0.0.1', 'location': 'Central', 'policy_updated_ts': '2018-02-19T15:09:55.217Z', 'updated_policy_id': 'DCAE_alex.Config_MS_alex_microservice', 'policy_updated_to_ver': '132', 'priority': '4', 'policy_updated_from_ver': '131', 'templateVersion': '2', 'uuid': '5e87d7c5-0daf-4b6b-ab92-5365cf5db1ef'}, 'property': None, 'type': 'JSON', 'policyVersion': '132'}, {'policyName': 'DCAE_alex.Config_db_client_policy_id_value.133.xml', 'policyConfigMessage': 'Config Retrieved! ', 'responseAttributes': {}, 'policyConfigStatus': 'CONFIG_RETRIEVED', 'matchingConditions': {'ONAPName': 'DCAE', 'Name': 'DCAE', 'ConfigName': 'alex_config_name'}, 'config': {'db_client_ts': '2017-11-21T12:12:13.696Z', 'db_client': 'ipsum', 'policy_hello': 'world!', 'policy_updated_from_ver': '132', 'updated_policy_id': 'DCAE_alex.Config_db_client_policy_id_value', 'policy_updated_ts': '2018-02-19T15:09:55.812Z', 'policy_updated_to_ver': '133'}, 'property': None, 'type': 'JSON', 'policyVersion': '133'}], 'event': {'action': 'gathered', 'timestamp': '2018-02-19 15:36:44.877380', 'update_id': 'bb73c20a-5ff8-450f-8223-da6720ade267', 'policies_count': 2}}} - - assert cbsclient.get('service_component_all/{0}'.format(TEST_NAME)).json == withstuff - - assert cbsclient.get('service_component_all/test_resolve_scn').json == {"config": expected_config} - - res = cbsclient.get('/service_component_all/{0}'.format(TEST_NAME)) - assert json.loads(res.data) == withstuff - assert res.json == withstuff - assert res.status_code == 200 - assert "x-onap-requestid" in res.headers - - res = cbsclient.get('/service_component_all/test_resolve_scn') - assert res.status_code == 200 - assert res.json == {"config": expected_config} - - res = cbsclient.get('/service_component_all/scn_NOTexists') - assert res.status_code == 404 - assert "x-onap-requestid" in res.headers diff --git a/app/app/tests/test_client.py b/app/app/tests/test_client.py deleted file mode 100644 index 96c3467..0000000 --- a/app/app/tests/test_client.py +++ /dev/null @@ -1,206 +0,0 @@ -# ============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========================================================= -# - -from config_binding_service import client -import pytest - - -# pytest doesnt support objects in conftest -class FakeReq(object): - """used to fake the logging params""" - def __init__(self): - self.path = "/unittest in {0}".format(__name__) - self.host = "localhost" - self.remote_addr = "6.6.6.6" - - -# pytest doesnt support objects in conftest -class FakeConnexion(object): - def __init__(self, headers, path, host, remote_addr): - self.headers = headers - self.path = path - self.host = host - self.remote_addr = remote_addr - - -def test_consul_get_all_as_transaction(monkeypatch, monkeyed_requests_put): - """tests _consul_get_all_as_transaction""" - monkeypatch.setattr('requests.put', monkeyed_requests_put) - allk = client._consul_get_all_as_transaction( - "test_service_component_name.unknown.unknown.unknown.dcae.onap.org", FakeReq(), "unit test xer") - assert allk == { - 'test_service_component_name.unknown.unknown.unknown.dcae.onap.org': {'my': 'amazing config'}, - 'test_service_component_name.unknown.unknown.unknown.dcae.onap.org:dti': {'my': 'dti'}, - 'test_service_component_name.unknown.unknown.unknown.dcae.onap.org:dmaap': {'foo': 'bar'}, - 'test_service_component_name.unknown.unknown.unknown.dcae.onap.org:policies/event': {'action': 'gathered', 'timestamp': '2018-02-19 15:36:44.877380', 'update_id': 'bb73c20a-5ff8-450f-8223-da6720ade267', 'policies_count': 2}, - 'test_service_component_name.unknown.unknown.unknown.dcae.onap.org:policies/items/DCAE_alex.Config_MS_alex_microservice': {'policyName': 'DCAE_alex.Config_MS_alex_microservice.132.xml', 'policyConfigMessage': 'Config Retrieved! ', 'responseAttributes': {}, 'policyConfigStatus': 'CONFIG_RETRIEVED', 'matchingConditions': {'ONAPName': 'DCAE', 'Name': 'DCAE', 'ConfigName': 'alex_config_name'}, 'config': {'policyScope': 'alex_policy_scope', 'configName': 'alex_config_name', 'description': 'test DCAE policy-handler', 'service': 'alex_service', 'policyName': 'alex_policy_name', 'riskLevel': '3', 'key1': 'value1', 'policy_hello': 'world!', 'content': {'foo': 'microservice3', 'foo_updated': '2018-01-30T13:25:33.222Z'}, 'riskType': '1712_ETE', 'guard': 'False', 'version': '0.0.1', 'location': 'Central', 'policy_updated_ts': '2018-02-19T15:09:55.217Z', 'updated_policy_id': 'DCAE_alex.Config_MS_alex_microservice', 'policy_updated_to_ver': '132', 'priority': '4', 'policy_updated_from_ver': '131', 'templateVersion': '2', 'uuid': '5e87d7c5-0daf-4b6b-ab92-5365cf5db1ef'}, 'property': None, 'type': 'JSON', 'policyVersion': '132'}, - 'test_service_component_name.unknown.unknown.unknown.dcae.onap.org:policies/items/DCAE_alex.Config_db_client_policy_id_value': {'policyName': 'DCAE_alex.Config_db_client_policy_id_value.133.xml', 'policyConfigMessage': 'Config Retrieved! ', 'responseAttributes': {}, 'policyConfigStatus': 'CONFIG_RETRIEVED', 'matchingConditions': {'ONAPName': 'DCAE', 'Name': 'DCAE', 'ConfigName': 'alex_config_name'}, 'config': {'db_client_ts': '2017-11-21T12:12:13.696Z', 'db_client': 'ipsum', 'policy_hello': 'world!', 'policy_updated_from_ver': '132', 'updated_policy_id': 'DCAE_alex.Config_db_client_policy_id_value', 'policy_updated_ts': '2018-02-19T15:09:55.812Z', 'policy_updated_to_ver': '133'}, 'property': None, 'type': 'JSON', 'policyVersion': '133'}, - 'test_service_component_name.unknown.unknown.unknown.dcae.onap.org:rels': ['my.amazing.relationship'] - } - - allk = client._consul_get_all_as_transaction("cbs_test_messed_up", FakeReq(), "unit test xer") - assert allk == {'cbs_test_messed_up': {'foo': 'bar'}, - 'cbs_test_messed_up:badkey': 'INVALID JSON'} - - -def test_get_config_rels_dmaap(monkeypatch, monkeyed_requests_put): - monkeypatch.setattr('requests.put', monkeyed_requests_put) - assert ({"foo3": "bar3"}, ["foo"], {"foo4": "bar4"}) == client._get_config_rels_dmaap("scn_exists", FakeReq(), "unit test xer") - assert ({"foo5": "bar5"}, [], {}) == client._get_config_rels_dmaap("scn_exists_nord", FakeReq(), "unit test xer") - - -def test_bad_config_http(): - test_config = {'yeahhhhh': "{{}}"} - test_rels = ["testing_bravo.somedomain.com"] - assert {'yeahhhhh': []} == client.resolve_override(test_config, test_rels) - - -def test_bad_config_dmaap(): - test_config = {'darkness': "<<>>"} - test_dmaap = {"WHO?": "darkness"} - assert {'darkness': {}} == client.resolve_override(test_config, test_dmaap) - - -def test_config_with_list(monkeypatch, monkeyed_get_connection_info_from_consul): - monkeypatch.setattr('config_binding_service.client._get_connection_info_from_consul', - monkeyed_get_connection_info_from_consul) - test_config_1 = {"dcae_target_type": [ - "vhss-ems", "pcrf-oam"], "downstream-laika": "{{ laika }}", "some-param": "Lorem ipsum dolor sit amet"} - test_rels_1 = ["3df5292249ae4a949f173063617cea8d_docker-snmp-polling-firstnet-m"] - test_bind_1 = client.resolve_override(test_config_1, test_rels_1, {}) - assert(test_bind_1 == {'dcae_target_type': [ - 'vhss-ems', 'pcrf-oam'], 'downstream-laika': [], 'some-param': 'Lorem ipsum dolor sit amet'}) - - test_config_2 = {"foo": ["{{cdap}}", "notouching", "<>"]} - test_rels_2 = ["cdap"] - test_dmaap_2 = {"yo": "im here"} - test_bind_2 = client.resolve_override(test_config_2, test_rels_2, test_dmaap_2) - assert(test_bind_2 == {"foo": [['666.666.666.666:666'], "notouching", "im here"]}) - - -def test_cdap(monkeypatch, monkeyed_get_connection_info_from_consul): - # user override to test CDAP functionality - monkeypatch.setattr('config_binding_service.client._get_connection_info_from_consul', - monkeyed_get_connection_info_from_consul) - test_rels = ["testing_alpha.somedomain.com", "testing_bravo.somedomain.com", - "testing_charlie.somedomain.com", "testing_charlie.somedomain.com", "cdap"] - test_config = {"streams_publishes": "{{alpha}}", - # should be dumped - "services_calls": [{"somekey": "{{charlie}}"}], - "cdap_to_manage": {'some_nested_thing': "{{cdap}}"}} # no dumps - test_bind_1 = client.resolve_override(test_config, test_rels) - assert test_bind_1 == {'services_calls': [{"somekey": ["5.5.5.5:555", "5.5.5.5:555"]}], 'streams_publishes': [ - "6.6.6.6:666"], 'cdap_to_manage': {'some_nested_thing': ['666.666.666.666:666']}} - assert test_bind_1['services_calls'] == [{"somekey": ["5.5.5.5:555", "5.5.5.5:555"]}] - assert test_bind_1['streams_publishes'] == ["6.6.6.6:666"] - - -def test_multiple_service_types(monkeypatch, monkeyed_get_connection_info_from_consul): - # test {{x,y,z}} - monkeypatch.setattr('config_binding_service.client._get_connection_info_from_consul', - monkeyed_get_connection_info_from_consul) - - # test 1: they all resovle - test_rels = ["testing_alpha.somedomain.com", "testing_bravo.somedomain.com", - "testing_charlie.somedomain.com", "testing_charlie.somedomain.com"] - config = {"ALL YOUR SERVICE BELONG TO US": "{{alpha,bravo,charlie}}"} - test_bind_1 = client.resolve_override(config, test_rels) - assert(test_bind_1 == {"ALL YOUR SERVICE BELONG TO US": [ - '6.6.6.6:666', '7.7.7.7:777', '5.5.5.5:555', '5.5.5.5:555']}) - - # test 2: two resolve, one is missing from rels key - config2 = {"two there one not exist": "{{alpha,bravo,notexist}}"} - test_bind_2 = client.resolve_override(config2, test_rels) - assert(test_bind_2 == {"two there one not exist": [ - '6.6.6.6:666', '7.7.7.7:777']}) - - # test 3: two resolve, one is in rels key but not registered - config3 = {"two there one unregistered": "{{alpha,bravo,unregistered}}"} - test_rels3 = ["testing_alpha.somedomain.com", - "testing_bravo.somedomain.com", "unregistered.somedomain.com"] - test_bind_3 = client.resolve_override(config3, test_rels3) - assert(test_bind_3 == {"two there one unregistered": [ - '6.6.6.6:666', '7.7.7.7:777']}) - - -def test_dmaap(monkeypatch): - # test resolving dmaap key - config = {"TODAY IS YOUR LUCKY DAY": "<>"} - # does not match - test_bind = client.resolve_override( - config, dmaap={"XX": "ABSOLVEME"}) # XX != XXX - assert(test_bind == {"TODAY IS YOUR LUCKY DAY": {}}) - # matches - test_bind_2 = client.resolve_override(config, dmaap={"XXX": "ABSOLVEME"}) - assert(test_bind_2 == {"TODAY IS YOUR LUCKY DAY": "ABSOLVEME"}) - - -def test_config(monkeypatch, monkeyed_get_connection_info_from_consul): - # test config override - monkeypatch.setattr('config_binding_service.client._get_connection_info_from_consul', - monkeyed_get_connection_info_from_consul) - test_config = {"autoderegisterafter": "10m", "cdap_to_manage": { - 'some_nested_thing': "{{cdap}}"}, "bindingttw": 5, "hcinterval": "5s"} - test_rels = ["cdap"] - test_bind_1 = client.resolve_override(test_config, test_rels) - assert test_bind_1 == {'autoderegisterafter': '10m', 'cdap_to_manage': { - 'some_nested_thing': ['666.666.666.666:666']}, 'bindingttw': 5, 'hcinterval': '5s'} - - -def test_non_existent(monkeypatch, monkeyed_get_connection_info_from_consul): - # test a valid config-rels but the key is not in Consul - monkeypatch.setattr('config_binding_service.client._get_connection_info_from_consul', - monkeyed_get_connection_info_from_consul) - test_config = {"you shall not be fufilled": "{{nonexistent_hope}}"} - # hopefully not registered in Consul.. - test_rels = ["nonexistent_hope.rework-central.ecomp.somedomain.com"] - test_bind_1 = client.resolve_override(test_config, test_rels, {}) - assert(test_bind_1 == {"you shall not be fufilled": []}) - - -def test_broker_redirect(monkeypatch, monkeyed_get_connection_info_from_consul): - # test the broker redirect - monkeypatch.setattr('config_binding_service.client._get_connection_info_from_consul', - monkeyed_get_connection_info_from_consul) - test_config = {"gimmie_dat_cdap": "{{cdap_serv}}"} - test_rels = ["cdap_serv.dcae.ecomp.somedomain.com"] - assert {"gimmie_dat_cdap": ['http://1.1.1.1:444/application/cdap_serv.dcae.ecomp.somedomain.com'] - } == client.resolve_override(test_config, test_rels) - - -def test_both(monkeypatch, monkeyed_get_connection_info_from_consul, expected_config): - # test rels and http - monkeypatch.setattr('config_binding_service.client._get_connection_info_from_consul', - monkeyed_get_connection_info_from_consul) - test_rels = ["testing_alpha.somedomain.com", "testing_bravo.somedomain.com", - "testing_charlie.somedomain.com", "testing_charlie.somedomain.com"] - test_dmaap = {"WHO?": "darkness"} - config = {"deep": {"ALL YOUR SERVICE BELONG TO US": "{{alpha,bravo,charlie}}"}, - "doubledeep": {"sodeep": {"hello": "<>"}}} - test_bind_1 = client.resolve_override(config, test_rels, test_dmaap) - assert(test_bind_1 == expected_config) - - -def test_failures(monkeypatch, monkeyed_requests_put, monkeyed_requests_get): - monkeypatch.setattr('requests.put', monkeyed_requests_put) - monkeypatch.setattr('requests.get', monkeyed_requests_get) - monkeypatch.setattr('connexion.request', FakeConnexion({"x-onap-requestid": 123456789}, "/service_component", "mytestingmachine", "myremoteclient")) - assert(client.resolve("scn_exists", FakeReq(), "unit test xer") == {"foo3": "bar3"}) - with pytest.raises(client.CantGetConfig): - client.resolve("scn_NOTexists", FakeReq(), "unit test xer") - with pytest.raises(client.CantGetConfig): - client.get_key("nokeyforyou", "test_service_component_name.unknown.unknown.unknown.dcae.onap.org", FakeReq(), "unit test xer") 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 @@ - - - - 4.0.0 - - org.onap.dcaegen2.platform - configbinding - 2.3.0-SNAPSHOT - - - - org.onap.dcaegen2.platform.configbinding - app - dcaegen2-platform-configbinding-app - 2.3.0-SNAPSHOT - http://maven.apache.org - - pom - - app - - - - UTF-8 - - http://localhost:9000 - - - - ${project.artifactId}-${project.version} - - - - org.codehaus.mojo - exec-maven-plugin - 1.2.1 - - ${session.executionRootDirectory}/mvn-phase-script.sh - - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${onap.nexus.url} - ${onap.nexus.rawrepo.baseurl.upload} - ${onap.nexus.rawrepo.baseurl.download} - ${onap.nexus.rawrepo.serverid} - ${onap.nexus.dockerregistry.snapshot} - ${onap.nexus.dockerregistry.release} - ${onap.nexus.dockerregistry.snapshot.serverid} - ${onap.nexus.dockerregistry.release.serverid} - ${onap.nexus.pypiserver.baseurl} - ${onap.nexus.pypiserver.serverid} - - - - - - - - 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/config_binding_service/__init__.py b/config_binding_service/__init__.py new file mode 100644 index 0000000..306a762 --- /dev/null +++ b/config_binding_service/__init__.py @@ -0,0 +1,46 @@ +# ============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. + +import os +import connexion + + +class BadEnviornmentENVNotFound(Exception): + """ + Specific exception to be raised when a required ENV varaible is missing + """ + pass + + +def get_consul_uri(): + """ + This method waterfalls reads an envioronmental variable called CONSUL_HOST + If that doesn't work, it raises an Exception + """ + if "CONSUL_HOST" in os.environ: + # WARNING! TODO! Currently the env file does not include the port. + # But some other people think that the port should be a part of that. + # For now, I'm hardcoding 8500 until this gets resolved. + return "http://{0}:{1}".format(os.environ["CONSUL_HOST"], 8500) + else: + raise BadEnviornmentENVNotFound("CONSUL_HOST") + + +# this has to be here due to circular dependency +app = connexion.App(__name__, specification_dir='.') +app.add_api('openapi.yaml', arguments={'title': 'Config Binding Service'}) diff --git a/config_binding_service/client.py b/config_binding_service/client.py new file mode 100644 index 0000000..c6a6753 --- /dev/null +++ b/config_binding_service/client.py @@ -0,0 +1,297 @@ +# ============LICENSE_START======================================================= +# Copyright (c) 2017-2018 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. + +import re +from functools import partial, reduce +import base64 +import copy +import json +import requests +import six +from config_binding_service import get_consul_uri +from config_binding_service.logging import utc, metrics + + +CONSUL = get_consul_uri() + +template_match_rels = re.compile("\{{2}([^\}\{]*)\}{2}") +template_match_dmaap = re.compile("<{2}([^><]*)>{2}") + +### +# Cusom Exception +### + + +class CantGetConfig(Exception): + """ + Represents an exception where a required key in consul isn't there + """ + + def __init__(self, code, response): + self.code = code + self.response = response + + +class BadRequest(Exception): + """ + Exception to be raised when the user tried to do something they shouldn't + """ + + def __init__(self, response): + self.code = 400 + self.response = response + + +### +# Private Functions +### + + +def _consul_get_all_as_transaction(service_component_name, raw_request, xer): + """ + Use Consul's transaction API to get all keys of the form service_component_name:* + Return a dict with all the values decoded + """ + payload = [ + { + "KV": { + "Verb": "get-tree", + "Key": service_component_name, + } + }] + + bts = utc() + response = requests.put("{0}/v1/txn".format(CONSUL), json=payload) + metrics(raw_request, bts, xer, "Consul", "/v1/txn".format(service_component_name), response.status_code, __name__, msg="Retrieving Consul transaction for all keys for {0}".format(service_component_name)) + + try: + response.raise_for_status() + except requests.exceptions.HTTPError as exc: + raise CantGetConfig(exc.response.status_code, exc.response.text) + + result = json.loads(response.text)['Results'] + + new_res = {} + for res in result: + key = res["KV"]["Key"] + val = base64.b64decode(res["KV"]["Value"]).decode("utf-8") + try: + new_res[key] = json.loads(val) + except json.decoder.JSONDecodeError: + new_res[key] = "INVALID JSON" # TODO, should we just include the original value somehow? + + if service_component_name not in new_res: + raise CantGetConfig(404, "") + + return new_res + + +def _get_config_rels_dmaap(service_component_name, raw_request, xer): + allk = _consul_get_all_as_transaction(service_component_name, raw_request, xer) + config = allk[service_component_name] + rels = allk.get(service_component_name + ":rels", []) + dmaap = allk.get(service_component_name + ":dmaap", {}) + return config, rels, dmaap + + +def _get_connection_info_from_consul(service_component_name): + """ + Call consul's catalog + TODO: currently assumes there is only one service + + DEPRECATION NOTE: + This function existed when DCAE was using Consul to resolve service component's connection information. + This relied on a "rels" key and a Cloudify relationship plugin to set up the magic. + The consensous is that this feature is no longer used. + This functionality is very likely deprecated by Kubernetes service discovery mechanism, and DMaaP. + + This function also includes logic related to CDAP, which is also likely deprecated. + + This code shall remain here for now but is at risk of being deleted in a future release. + """ + # Note: there should be a metrics log here, but see the deprecation note above; this function is due to be deleted. + res = requests.get("{0}/v1/catalog/service/{1}".format(CONSUL, service_component_name)) + res.raise_for_status() + services = res.json() + if services == []: + return None # later will get filtered out + ip_addr = services[0]["ServiceAddress"] + port = services[0]["ServicePort"] + + if "cdap_app" in service_component_name: + redirectish_url = "http://{0}:{1}/application/{2}".format(ip_addr, port, service_component_name) + res = requests.get(redirectish_url) + res.raise_for_status() + details = res.json() + # Pick out the details to expose to the component developers. These keys come from the broker API + return {key: details[key] for key in ["connectionurl", "serviceendpoints"]} + return "{0}:{1}".format(ip_addr, port) + + +def _replace_rels_template(rels, template_identifier): + """ + The magic. Replaces a template identifier {{...}} with the entrie(s) from the rels keys + NOTE: There was a discussion over whether the CBS should treat {{}} as invalid. Mike asked that + it resolve to the empty list. So, it does resolve it to empty list. + """ + returnl = [] + for rel in rels: + if template_identifier in rel and template_identifier != "": + returnl.append(rel) + # returnl now contains a list of DNS names (possible empty), now resolve them (or not if they are not regustered) + return list(filter(lambda x: x is not None, map(_get_connection_info_from_consul, returnl))) + + +def _replace_dmaap_template(dmaap, template_identifier): + """ + This one liner could have been just put inline in the caller but maybe this will get more complex in future + Talked to Mike, default value if key is not found in dmaap key should be {} + """ + return {} if (template_identifier not in dmaap or template_identifier == "<<>>") else dmaap[template_identifier] + + +def _replace_value(v, rels, dmaap): + """ + Takes a value v that was some value in the templatized configuration, determines whether it needs replacement (either {{}} or <<>>), and if so, replaces it. + Otherwise just returns v + + implementation notes: + - the split below sees if we have v = x,y,z... so we can support {{x,y,z,....}} + - the lambda is because we can't fold operators in Python, wanted fold(+, L) where + when applied to lists in python is list concatenation + """ + if isinstance(v, six.string_types): # do not try to replace anything that is not a string + match_on_rels = re.match(template_match_rels, v) + if match_on_rels: + # now holds just x,.. of {{x,...}} + template_identifier = match_on_rels.groups()[0].strip() + rtpartial = partial(_replace_rels_template, rels) + return reduce(lambda a, b: a + b, map(rtpartial, template_identifier.split(",")), []) + match_on_dmaap = re.match(template_match_dmaap, v) + if match_on_dmaap: + template_identifier = match_on_dmaap.groups()[0].strip() + """ + Here is what Mike said: + 1) want simple replacement of "<< >>" with dmaap key value + 2) never need to support <> whereas we do support {{sct1,sct2}} + The consequence is that if you give the CBS a dmaap key like {"foo" : {...}} you are going to get back {...}, but rels always returns [...]. + So now component developers have to possible handle dicts and [], and we have to communicate that to them + """ + return _replace_dmaap_template(dmaap, template_identifier) + return v # was not a match or was not a string, return value as is + + +def _recurse(config, rels, dmaap): + """ + Recurse throug a configuration, or recursively a sub elemebt of it. + If it's a dict: recurse over all the values + If it's a list: recurse over all the values + If it's a string: return the replacement + If none of the above, just return the item. + """ + if isinstance(config, list): + return [_recurse(item, rels, dmaap) for item in config] + if isinstance(config, dict): + for key in config: + config[key] = _recurse(config[key], rels, dmaap) + return config + if isinstance(config, six.string_types): + return _replace_value(config, rels, dmaap) + # not a dict, not a list, not a string, nothing to do. + return config + + +######### +# PUBLIC API +######### + + +def resolve(service_component_name, raw_request, xer): + """ + Return the bound config of service_component_name + + raw_request and xer are needed to form the correct metrics log + """ + config, rels, dmaap = _get_config_rels_dmaap(service_component_name, raw_request, xer) + return _recurse(config, rels, dmaap) + + +def resolve_override(config, rels=[], dmaap={}): + """ + Explicitly take in a config, rels, dmaap and try to resolve it. + Useful for testing where you dont want to put the test values in consul + """ + # use deepcopy to make sure that config is not touched + return _recurse(copy.deepcopy(config), rels, dmaap) + + +def resolve_all(service_component_name, raw_request, xer): + """ + Return config, policies, and any other k such that service_component_name:k exists (other than :dmaap and :rels) + + raw_request and xer are needed to form the correct metrics log + """ + allk = _consul_get_all_as_transaction(service_component_name, raw_request, xer) + returnk = {} + + # replace the config with the resolved config + returnk["config"] = resolve_override(allk[service_component_name], + allk.get("{0}:rels".format(service_component_name), []), + allk.get("{0}:dmaap".format(service_component_name), {})) + + # concatenate the items + for k in allk: + if "policies" in k: + if "policies" not in returnk: + returnk["policies"] = {} + returnk["policies"]["event"] = {} + returnk["policies"]["items"] = [] + + if k.endswith(":policies/event"): + returnk["policies"]["event"] = allk[k] + elif ":policies/items" in k: + returnk["policies"]["items"].append(allk[k]) + else: + if not(k == service_component_name or k.endswith(":rels") or k.endswith(":dmaap")): + # this would blow up if you had a key in consul without a : but this shouldnt happen + suffix = k.split(":")[1] + returnk[suffix] = allk[k] + + return returnk + + +def get_key(key, service_component_name, raw_request, xer): + """ + Try to fetch a key k from Consul of the form service_component_name:k + + raw_request and xer are needed to form the correct metrics log + """ + if key == "policies": + raise BadRequest( + ":policies is a complex folder and should be retrieved using the service_component_all API") + + bts = utc() + path = "v1/kv/{0}:{1}".format(service_component_name, key) + response = requests.get("{0}/{1}".format(CONSUL, path)) + metrics(raw_request, bts, xer, "Consul", path, response.status_code, __name__, msg="Retrieving single Consul key {0} for {1}".format(key, service_component_name)) + + try: + response.raise_for_status() + except requests.exceptions.HTTPError as exc: + raise CantGetConfig(exc.response.status_code, exc.response.text) + rest = json.loads(response.text)[0] + return json.loads(base64.b64decode(rest["Value"]).decode("utf-8")) diff --git a/config_binding_service/controller.py b/config_binding_service/controller.py new file mode 100644 index 0000000..c2eb21c --- /dev/null +++ b/config_binding_service/controller.py @@ -0,0 +1,108 @@ +# ============LICENSE_START======================================================= +# Copyright (c) 2017-2018 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. + +import json +import requests +import connexion +import uuid +from flask import Response +from config_binding_service import client, get_consul_uri +from config_binding_service.logging import audit, utc, error, metrics + + +def _get_helper(json_expecting_func, **kwargs): + """ + Helper function used by several functions below + """ + try: + payload = json_expecting_func(**kwargs) + response, status_code, mimetype = json.dumps(payload), 200, "application/json" + except client.BadRequest as exc: + response, status_code, mimetype = exc.response, exc.code, "text/plain" + except client.CantGetConfig as exc: + response, status_code, mimetype = exc.response, exc.code, "text/plain" + except Exception: + response, status_code, mimetype = "Unknown error", 500, "text/plain" + return response, status_code, mimetype + + +def _get_or_generate_xer(raw_request): + """get or generate the transaction id""" + xer = raw_request.headers.get("x-onap-requestid", None) + if xer is None: + # some components are still using the old name + xer = raw_request.headers.get("x-ecomp-requestid", None) + if xer is None: + # the user did NOT supply a request id, generate one + xer = str(uuid.uuid4()) + return xer + + +def bind_all(service_component_name): + """ + Get all the keys in Consul for this SCN, and bind the config + """ + xer = _get_or_generate_xer(connexion.request) + bts = utc() + response, status_code, mimetype = _get_helper(client.resolve_all, service_component_name=service_component_name, raw_request=connexion.request, xer=xer) + audit(connexion.request, bts, xer, status_code, __name__, "called for component {0}".format(service_component_name)) + # Even though some older components might be using the ecomp name, we return the proper one + return Response(response=response, status=status_code, mimetype=mimetype, headers={"x-onap-requestid": xer}) + + +def bind_config_for_scn(service_component_name): + """ + Bind just the config for this SCN + """ + xer = _get_or_generate_xer(connexion.request) + bts = utc() + response, status_code, mimetype = _get_helper(client.resolve, service_component_name=service_component_name, raw_request=connexion.request, xer=xer) + audit(connexion.request, bts, xer, status_code, __name__, "called for component {0}".format(service_component_name)) + return Response(response=response, status=status_code, mimetype=mimetype, headers={"x-onap-requestid": xer}) + + +def get_key(key, service_component_name): + """ + Get a single key k of the form service_component_name:k from Consul. + Should not be used and will return a BAD REQUEST for k=policies because it's a complex object + """ + xer = _get_or_generate_xer(connexion.request) + bts = utc() + response, status_code, mimetype = _get_helper(client.get_key, key=key, service_component_name=service_component_name, raw_request=connexion.request, xer=xer) + audit(connexion.request, bts, xer, status_code, __name__, "called for component {0}".format(service_component_name)) + return Response(response=response, status=status_code, mimetype=mimetype, headers={"x-onap-requestid": xer}) + + +def healthcheck(): + """ + CBS Healthcheck + """ + xer = _get_or_generate_xer(connexion.request) + path = "v1/catalog/service/config_binding_service" + bts = utc() + res = requests.get("{0}/{1}".format(get_consul_uri(), path)) + status = res.status_code + if status == 200: + msg = "CBS is alive and Consul connection OK" + else: + msg = "CBS is alive but cannot reach Consul" + # treating this as a WARN because this could be a temporary network glitch. Also per EELF guidelines this is a 200 ecode (availability) + error(connexion.request, xer, "WARN", 200, tgt_entity="Consul", tgt_path="/v1/catalog/service/config_binding_service", msg=msg) + metrics(connexion.request, bts, xer, "Consul", path, res.status_code, __name__, msg="Checking Consul connectivity during CBS healthcheck, {0}".format(msg)) + audit(connexion.request, bts, xer, status, __name__, msg=msg) + return Response(response=msg, status=status) diff --git a/config_binding_service/logging.py b/config_binding_service/logging.py new file mode 100644 index 0000000..35750f2 --- /dev/null +++ b/config_binding_service/logging.py @@ -0,0 +1,237 @@ +# ============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. + +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") + +# 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): + """ + Create a RotatingFileHandler and a streamhandler for stdout + https://docs.python.org/3/library/logging.handlers.html + what's with the non-pythonic naming in these stdlib methods? Shameful. + """ + 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 + This is *not* launched during unit testing, so unit tests do not create/write log files + """ + makedirs("/opt/logs", exist_ok=True) + + # create the audit log + aud_file = "/opt/logs/audit.log" + 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" + 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" + global _METRICS_LOGGER + _METRICS_LOGGER = _create_logger("config_binding_service_metrics", met_file) + + +def utc(): + """gets current time in utc""" + return datetime.datetime.utcnow() + + +def audit(raw_request, bts, xer, rcode, calling_mod, msg="n/a"): + """ + write an EELF audit record per https://wiki.onap.org/download/attachments/1015849/ONAP%20application%20logging%20guidelines.pdf?api=v2 + %The audit fields implemented: + + 1 BeginTimestamp Implemented (bts) + 2 EndTimestamp Auto Injected when this is called + 3 RequestID Implemented (xer) + 5 threadId n/a + 7 serviceName Implemented (from Req) + 9 StatusCode Auto injected based on rcode + 10 ResponseCode Implemented (rcode) + 13 Category log level - all audit records are INFO. + 15 Server IP address Implemented (from Req) + 16 ElapsedTime Auto Injected (milliseconds) + 17 Server This is running in a Docker container so this is not applicable, my HOSTNAME is always "config_binding_service" + 18 ClientIPaddress Implemented (from Req) + 19 class name Implemented (mod), though docs say OOP, I am using the python module here + 20 Unused ...implemented.... + 21-25 Custom n/a + 26 detailMessage Implemented (msg) + + Not implemented + 4 serviceInstanceID - ? + 6 physical/virtual server name (Optional) + 8 PartnerName - nothing in the request tells me this + 11 Response Description - the CBS follows standard HTTP error codes so look them up + 12 instanceUUID - Optional + 14 Severity (Optional) + """ + 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, + ) + ) + + +def error(raw_request, xer, severity, ecode, tgt_entity="n/a", tgt_path="n/a", msg="n/a", adv_msg="n/a"): + """ + write an EELF error record per + the error fields implemented: + + 1 Timestamp Auto Injected when this is called + 2 RequestID Implemented (xer) + 3 ThreadID n/a + 4 ServiceName Implemented (from Req) + 6 TargetEntity Implemented (tgt_entity) + 7 TargetServiceName Implemented (tgt_path)/ + 8 ErrorCategory Implemented (severity) + 9. ErrorCode Implemented (ecode) + 10 ErrorDescription Implemented (msg) + 11. detailMessage Implemented (adv_msg) + + Not implemented: + 5 PartnerName - nothing in the request tells me this + """ + 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, + ) + ) + + +def metrics(raw_request, bts, xer, target, target_path, rcode, calling_mod, msg="n/a"): + """ + write an EELF metrics record per https://wiki.onap.org/download/attachments/1015849/ONAP%20application%20logging%20guidelines.pdf?api=v2 + %The metrics fields implemented: + + 1 BeginTimestamp Implemented (bts) + 2 EndTimestamp Auto Injected when this is called + 3 RequestID Implemented (xer) + 5 threadId n/a + 7 serviceName Implemented (from Req) + 9 TargetEntity Implemented (target) + 10 TargetServiceName Implemented (target_path) + 11 StatusCode Implemented (based on rcode) + 12 Response Code Implemented (rcode) + 15 Category log level all metrics records are INFO. + 17 Server IP address Implemented (from Req) + 18 ElapsedTime Auto Injected (milliseconds) + 19 Server This is running in a Docker container so this is not applicable, my HOSTNAME is always "config_binding_service" + 20 ClientIPaddress Implemented (from Req) + 21 class name Implemented (mod), though docs say OOP, I am using the python module here + 22 Unused ...implemented.... + 24 TargetVirtualEntity n/a + 25-28 Custom n/a + 29 detailMessage Implemented (msg) + + Not implemented + 4 serviceInstanceID - ? + 6 physical/virtual server name (Optional) + 8 PartnerName - nothing in the request tells me this + 13 Response Description - the CBS follows standard HTTP error codes so look them up + 14 instanceUUID - Optional + 16 Severity (Optional) + 23 ProcessKey - optional + """ + 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, + ) + ) diff --git a/config_binding_service/openapi.yaml b/config_binding_service/openapi.yaml new file mode 100644 index 0000000..96b19e4 --- /dev/null +++ b/config_binding_service/openapi.yaml @@ -0,0 +1,112 @@ +openapi: 3.0.0 +info: + version: 2.4.0 + title: Config Binding Service +paths: + '/service_component/{service_component_name}': + parameters: + - name: service_component_name + in: path + description: >- + Service Component Name. service_component_name must be a key in + consul. + required: true + schema: + type: string + get: + description: >- + Binds the configuration for service_component_name and returns the bound + configuration as a JSON + operationId: config_binding_service.controller.bind_config_for_scn + responses: + '200': + description: OK; the bound config is returned as an object + content: + '*/*': + schema: + type: object + '404': + description: there is no configuration in Consul for this component + '/service_component_all/{service_component_name}': + parameters: + - name: service_component_name + in: path + description: >- + Service Component Name. service_component_name must be a key in + consul. + required: true + schema: + type: string + get: + description: >- + Binds the configuration for service_component_name and returns the bound + configuration, policies, and any other keys that are in Consul + operationId: config_binding_service.controller.bind_all + responses: + '200': + description: >- + OK; returns {config : ..., policies : ....., k : ...} for all other + k in Consul + content: + '*/*': + schema: + type: object + '404': + description: there is no configuration in Consul for this component + '/{key}/{service_component_name}': + parameters: + - name: key + in: path + description: >- + this endpoint tries to pull service_component_name:key; key is the key + after the colon + required: true + schema: + type: string + - name: service_component_name + in: path + description: Service Component Name. + required: true + schema: + type: string + get: + description: >- + this is an endpoint that fetches a generic service_component_name:key + out of Consul. The idea is that we don't want to tie components to + Consul directly in case we swap out the backend some day, so the CBS + abstracts Consul from clients. The structuring and weird collision of + this new API with the above is unfortunate but due to legacy concerns. + operationId: config_binding_service.controller.get_key + responses: + '200': + description: 'OK; returns service_component_name:key' + content: + '*/*': + schema: + type: object + '400': + description: >- + bad request. Currently this is only returned on :policies, which is + a complex object, and should be gotten through service_component_all + content: + '*/*': + schema: + type: string + '404': + description: key does not exist + content: + '*/*': + schema: + type: string + /healthcheck: + get: + description: >- + This is the health check endpoint. If this returns a 200, the server is + alive and consul can be reached. If not a 200, either dead, or no + connection to consul + operationId: config_binding_service.controller.healthcheck + responses: + '200': + description: Successful response + '503': + description: the config binding service cannot reach Consul diff --git a/config_binding_service/run.py b/config_binding_service/run.py new file mode 100755 index 0000000..175c0cf --- /dev/null +++ b/config_binding_service/run.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +# ============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. +import os +from gevent.pywsgi import WSGIServer +from config_binding_service.logging import create_loggers, DEBUG_LOGGER +from config_binding_service import app + + +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. --> 4.0.0 + - org.onap - dcaegen2 - 1.2.0-SNAPSHOT + org.onap.oparent + oparent + 2.0.0 org.onap.dcaegen2.platform configbinding dcaegen2-platform-configbinding - 2.3.0-SNAPSHOT + 2.4.0-SNAPSHOT http://maven.apache.org - - pom - - app - - UTF-8 - - http://localhost:9000 - - ** + . + xunit-results.xml + coverage.xml + xunit-results.xml + py + python + config_binding_service/*.py + tests/*,setup.py ${project.artifactId}-${project.version} + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.7 + + true + + + + org.apache.maven.plugins + maven-deploy-plugin + + 2.8 + + true + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M1 + + true + + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.6 + + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + true + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + default-jar + + + + + + + org.apache.maven.plugins + maven-install-plugin + 2.4 + + true + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12.4 + + true + + org.codehaus.mojo exec-maven-plugin @@ -55,7 +132,6 @@ ECOMP is a trademark and service mark of AT&T Intellectual Property. ${session.executionRootDirectory}/mvn-phase-script.sh - ${project.groupId} ${project.artifactId} @@ -64,17 +140,131 @@ ECOMP is a trademark and service mark of AT&T Intellectual Property. ${onap.nexus.rawrepo.baseurl.upload} ${onap.nexus.rawrepo.baseurl.download} ${onap.nexus.rawrepo.serverid} - ${onap.nexus.dockerregistry.snapshot} + ${onap.nexus.dockerregistry.daily} ${onap.nexus.dockerregistry.release} - ${onap.nexus.dockerregistry.snapshot.serverid} - ${onap.nexus.dockerregistry.release.serverid} - ${onap.nexus.pypiserver.baseurl} - ${onap.nexus.pypiserver.serverid} + + + + + org.codehaus.mojo + exec-maven-plugin + 1.2.1 + + + clean phase script + clean + + exec + + + + __ + clean + + + + + generate-sources script + generate-sources + + exec + + + + __ + generate-sources + + + + + compile script + compile + + exec + + + + __ + compile + + + + + package script + package + + exec + + + + __ + package + + + + + test script + test + + exec + + + + __ + test + + + + + install script + install + + exec + + + + __ + install + + + + + deploy script + deploy + + exec + + + + __ + deploy + + + + + + - diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5c71597 --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +# ============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. + +from setuptools import setup, find_packages + +setup( + 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", + url="https://gerrit.onap.org/r/#/admin/projects/dcaegen2/platform/configbinding", + 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/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..1875bf6 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,21 @@ +# ================================================================================ +# Copyright (c) 2018 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. + + +# empty __init__.py so that pytest can add correct path to coverage report, -- per pytest +# best practice guideline diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..c8f2a06 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,70 @@ +import pytest +from requests.exceptions import HTTPError +from config_binding_service import get_consul_uri + + +class FakeResponse(): + def __init__(self, status_code, text): + self.text = text + self.status_code = status_code + + def raise_for_status(self): + if self.status_code >= 400: + raise HTTPError(response=FakeResponse(404, "")) + + +@pytest.fixture +def expected_config(): + return {"deep": {"ALL YOUR SERVICE BELONG TO US": ['6.6.6.6:666', '7.7.7.7:777', '5.5.5.5:555', '5.5.5.5:555']}, + "doubledeep": {"sodeep": {"hello": "darkness"}}} + + +@pytest.fixture +def monkeyed_get_connection_info_from_consul(): + def _monkeyed_get_connection_info_from_consul(service_component_name): + # shared monkeypatch. probably somewhat lazy because the function htis patches can be broken up. + if service_component_name == "cdap": + return '666.666.666.666:666' + elif service_component_name == "testing_bravo.somedomain.com": + return '7.7.7.7:777' + elif service_component_name == "testing_alpha.somedomain.com": + return '6.6.6.6:666' + elif service_component_name == "testing_charlie.somedomain.com": + return '5.5.5.5:555' + elif service_component_name == "nonexistent_hope": + return None # the real function returns None here + elif service_component_name == "cdap_serv.dcae.ecomp.somedomain.com": + broker_ip = '1.1.1.1' + broker_port = 444 + return "http://{0}:{1}/application/{2}".format(broker_ip, broker_port, service_component_name) + return _monkeyed_get_connection_info_from_consul + + +@pytest.fixture +def monkeyed_requests_get(): + def _monkeyed_requests_get(url): + if url == "{0}/v1/kv/test_service_component_name.unknown.unknown.unknown.dcae.onap.org:dti".format(get_consul_uri()): + return FakeResponse(status_code=200, text='[{"LockIndex":0,"Key":"test_service_component_name.unknown.unknown.unknown.dcae.onap.org:dti","Flags":0,"Value": "eyJteSIgOiAiZHRpIn0=","CreateIndex":4066524,"ModifyIndex":4066524}]') + else: + return FakeResponse(status_code=404, text="") + return _monkeyed_requests_get + + +@pytest.fixture +def monkeyed_requests_put(): + def _monkeyed_requests_put(url, json): + if url == "{0}/v1/txn".format(get_consul_uri()): + key = json[0]["KV"]["Key"] + if key == "test_service_component_name.unknown.unknown.unknown.dcae.onap.org": + return FakeResponse(status_code=200, text='{"Results":[{"KV":{"LockIndex":0,"Key":"test_service_component_name.unknown.unknown.unknown.dcae.onap.org","Flags":0,"Value":"eyJteSIgOiAiYW1hemluZyBjb25maWcifQ==","CreateIndex":4051555,"ModifyIndex":4051555}},{"KV":{"LockIndex":0,"Key":"test_service_component_name.unknown.unknown.unknown.dcae.onap.org:dmaap","Flags":0,"Value":"eyJmb28iIDogImJhciJ9","CreateIndex":4051571,"ModifyIndex":4051571}},{"KV":{"LockIndex":0,"Key":"test_service_component_name.unknown.unknown.unknown.dcae.onap.org:dti","Flags":0,"Value":"eyJteSIgOiAiZHRpIn0=","CreateIndex":4066524,"ModifyIndex":4066524}},{"KV":{"LockIndex":0,"Key":"test_service_component_name.unknown.unknown.unknown.dcae.onap.org:policies/event","Flags":0,"Value":"eyJhY3Rpb24iOiAiZ2F0aGVyZWQiLCAidGltZXN0YW1wIjogIjIwMTgtMDItMTkgMTU6MzY6NDQuODc3MzgwIiwgInVwZGF0ZV9pZCI6ICJiYjczYzIwYS01ZmY4LTQ1MGYtODIyMy1kYTY3MjBhZGUyNjciLCAicG9saWNpZXNfY291bnQiOiAyfQ==","CreateIndex":4048564,"ModifyIndex":4048564}},{"KV":{"LockIndex":0,"Key":"test_service_component_name.unknown.unknown.unknown.dcae.onap.org:policies/items/DCAE_alex.Config_MS_alex_microservice","Flags":0,"Value":"eyJwb2xpY3lOYW1lIjogIkRDQUVfYWxleC5Db25maWdfTVNfYWxleF9taWNyb3NlcnZpY2UuMTMyLnhtbCIsICJwb2xpY3lDb25maWdNZXNzYWdlIjogIkNvbmZpZyBSZXRyaWV2ZWQhICIsICJyZXNwb25zZUF0dHJpYnV0ZXMiOiB7fSwgInBvbGljeUNvbmZpZ1N0YXR1cyI6ICJDT05GSUdfUkVUUklFVkVEIiwgIm1hdGNoaW5nQ29uZGl0aW9ucyI6IHsiT05BUE5hbWUiOiAiRENBRSIsICJOYW1lIjogIkRDQUUiLCAiQ29uZmlnTmFtZSI6ICJhbGV4X2NvbmZpZ19uYW1lIn0sICJjb25maWciOiB7InBvbGljeVNjb3BlIjogImFsZXhfcG9saWN5X3Njb3BlIiwgImNvbmZpZ05hbWUiOiAiYWxleF9jb25maWdfbmFtZSIsICJkZXNjcmlwdGlvbiI6ICJ0ZXN0IERDQUUgcG9saWN5LWhhbmRsZXIiLCAic2VydmljZSI6ICJhbGV4X3NlcnZpY2UiLCAicG9saWN5TmFtZSI6ICJhbGV4X3BvbGljeV9uYW1lIiwgInJpc2tMZXZlbCI6ICIzIiwgImtleTEiOiAidmFsdWUxIiwgInBvbGljeV9oZWxsbyI6ICJ3b3JsZCEiLCAiY29udGVudCI6IHsiZm9vIjogIm1pY3Jvc2VydmljZTMiLCAiZm9vX3VwZGF0ZWQiOiAiMjAxOC0wMS0zMFQxMzoyNTozMy4yMjJaIn0sICJyaXNrVHlwZSI6ICIxNzEyX0VURSIsICJndWFyZCI6ICJGYWxzZSIsICJ2ZXJzaW9uIjogIjAuMC4xIiwgImxvY2F0aW9uIjogIkNlbnRyYWwiLCAicG9saWN5X3VwZGF0ZWRfdHMiOiAiMjAxOC0wMi0xOVQxNTowOTo1NS4yMTdaIiwgInVwZGF0ZWRfcG9saWN5X2lkIjogIkRDQUVfYWxleC5Db25maWdfTVNfYWxleF9taWNyb3NlcnZpY2UiLCAicG9saWN5X3VwZGF0ZWRfdG9fdmVyIjogIjEzMiIsICJwcmlvcml0eSI6ICI0IiwgInBvbGljeV91cGRhdGVkX2Zyb21fdmVyIjogIjEzMSIsICJ0ZW1wbGF0ZVZlcnNpb24iOiAiMiIsICJ1dWlkIjogIjVlODdkN2M1LTBkYWYtNGI2Yi1hYjkyLTUzNjVjZjVkYjFlZiJ9LCAicHJvcGVydHkiOiBudWxsLCAidHlwZSI6ICJKU09OIiwgInBvbGljeVZlcnNpb24iOiAiMTMyIn0=","CreateIndex":4048564,"ModifyIndex":4065574}},{"KV":{"LockIndex":0,"Key":"test_service_component_name.unknown.unknown.unknown.dcae.onap.org:policies/items/DCAE_alex.Config_db_client_policy_id_value","Flags":0,"Value":"eyJwb2xpY3lOYW1lIjogIkRDQUVfYWxleC5Db25maWdfZGJfY2xpZW50X3BvbGljeV9pZF92YWx1ZS4xMzMueG1sIiwgInBvbGljeUNvbmZpZ01lc3NhZ2UiOiAiQ29uZmlnIFJldHJpZXZlZCEgIiwgInJlc3BvbnNlQXR0cmlidXRlcyI6IHt9LCAicG9saWN5Q29uZmlnU3RhdHVzIjogIkNPTkZJR19SRVRSSUVWRUQiLCAibWF0Y2hpbmdDb25kaXRpb25zIjogeyJPTkFQTmFtZSI6ICJEQ0FFIiwgIk5hbWUiOiAiRENBRSIsICJDb25maWdOYW1lIjogImFsZXhfY29uZmlnX25hbWUifSwgImNvbmZpZyI6IHsiZGJfY2xpZW50X3RzIjogIjIwMTctMTEtMjFUMTI6MTI6MTMuNjk2WiIsICJkYl9jbGllbnQiOiAiaXBzdW0iLCAicG9saWN5X2hlbGxvIjogIndvcmxkISIsICJwb2xpY3lfdXBkYXRlZF9mcm9tX3ZlciI6ICIxMzIiLCAidXBkYXRlZF9wb2xpY3lfaWQiOiAiRENBRV9hbGV4LkNvbmZpZ19kYl9jbGllbnRfcG9saWN5X2lkX3ZhbHVlIiwgInBvbGljeV91cGRhdGVkX3RzIjogIjIwMTgtMDItMTlUMTU6MDk6NTUuODEyWiIsICJwb2xpY3lfdXBkYXRlZF90b192ZXIiOiAiMTMzIn0sICJwcm9wZXJ0eSI6IG51bGwsICJ0eXBlIjogIkpTT04iLCAicG9saWN5VmVyc2lvbiI6ICIxMzMifQ==","CreateIndex":4048564,"ModifyIndex":4065570}},{"KV":{"LockIndex":0,"Key":"test_service_component_name.unknown.unknown.unknown.dcae.onap.org:rels","Flags":0,"Value":"WyJteS5hbWF6aW5nLnJlbGF0aW9uc2hpcCJd","CreateIndex":4051567,"ModifyIndex":4051567}}],"Errors":null,"Index":0,"LastContact":0,"KnownLeader":true}') + elif key == "scn_exists": + return FakeResponse(status_code=200, text='{"Results":[{"KV":{"LockIndex":0,"Key":"scn_exists","Flags":0,"Value":"eyJmb28zIiA6ICJiYXIzIn0=","CreateIndex":4067403,"ModifyIndex":4067403}},{"KV":{"LockIndex":0,"Key":"scn_exists:dmaap","Flags":0,"Value":"eyJmb280IiA6ICJiYXI0In0=","CreateIndex":4067410,"ModifyIndex":4067410}},{"KV":{"LockIndex":0,"Key":"scn_exists:rels","Flags":0,"Value":"WyJmb28iXQ==","CreateIndex":4067406,"ModifyIndex":4067406}},{"KV":{"LockIndex":0,"Key":"scn_exists_nord","Flags":0,"Value":"eyJmb281IiA6ICJiYXI1In0=","CreateIndex":4067340,"ModifyIndex":4067340}}],"Errors":null,"Index":0,"LastContact":0,"KnownLeader":true}') + elif key == "scn_exists_nord": + return FakeResponse(status_code=200, text='{"Results":[{"KV":{"LockIndex":0,"Key":"scn_exists_nord","Flags":0,"Value":"eyJmb281IiA6ICJiYXI1In0=","CreateIndex":4067340,"ModifyIndex":4067340}}],"Errors":null,"Index":0,"LastContact":0,"KnownLeader":true}') + elif key == "test_resolve_scn": + return FakeResponse(status_code=200, text='{"Results":[{"KV":{"LockIndex":0,"Key":"test_resolve_scn","Flags":0,"Value":"ewogICAgICAgICAgICAgICAgImRlZXAiIDogewogICAgICAgICAgICAgICAgICAgICJBTEwgWU9VUiBTRVJWSUNFIEJFTE9ORyBUTyBVUyIgOiAie3thbHBoYSxicmF2byxjaGFybGllfX0ifSwKICAgICAgICAgICAgICAgICJkb3VibGVkZWVwIiA6ICB7CiAgICAgICAgICAgICAgICAgICAgInNvZGVlcCIgOiB7ImhlbGxvIiA6ICI8PFdITz8+PiJ9fQogICAgICAgICAgICAgfQo=","CreateIndex":4068002,"ModifyIndex":4068002}},{"KV":{"LockIndex":0,"Key":"test_resolve_scn:dmaap","Flags":0,"Value":"eyJXSE8/IiA6ICJkYXJrbmVzcyJ9","CreateIndex":4068013,"ModifyIndex":4068013}},{"KV":{"LockIndex":0,"Key":"test_resolve_scn:rels","Flags":0,"Value":"WyJ0ZXN0aW5nX2FscGhhLnNvbWVkb21haW4uY29tIiwgInRlc3RpbmdfYnJhdm8uc29tZWRvbWFpbi5jb20iLCAidGVzdGluZ19jaGFybGllLnNvbWVkb21haW4uY29tIiwgInRlc3RpbmdfY2hhcmxpZS5zb21lZG9tYWluLmNvbSJd","CreateIndex":4068010,"ModifyIndex":4068010}}],"Errors":null,"Index":0,"LastContact":0,"KnownLeader":true}') + elif key == "cbs_test_messed_up": + return FakeResponse(status_code=200, text='{"Results":[{"KV":{"LockIndex":0,"Key":"cbs_test_messed_up","Flags":0,"Value":"eyJmb28iIDogImJhciJ9","CreateIndex":4864032,"ModifyIndex":4864052}},{"KV":{"LockIndex":0,"Key":"cbs_test_messed_up:badkey","Flags":0,"Value":"eyJub3QgYSBqc29ubm5uIFJFS1RUVFQ=","CreateIndex":4864075,"ModifyIndex":4864075}}],"Errors":null,"Index":0,"LastContact":0,"KnownLeader":true}') + elif key == "scn_NOTexists": + return FakeResponse(status_code=404, text="") + return _monkeyed_requests_put diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..118a2a0 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,103 @@ +# ============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. +import json +import pytest +import os +import tempfile +from config_binding_service import app + + +TEST_NAME = "test_service_component_name.unknown.unknown.unknown.dcae.onap.org" + + +# http://flask.pocoo.org/docs/1.0/testing/ +@pytest.fixture +def cbsclient(): + db_fd, app.app.config['DATABASE'] = tempfile.mkstemp() + app.app.config['TESTING'] = True + testclient = app.app.test_client() + + yield testclient + + os.close(db_fd) + os.unlink(app.app.config['DATABASE']) + + +def test_get(monkeypatch, cbsclient, monkeyed_requests_put): + monkeypatch.setattr('requests.put', monkeyed_requests_put) + + res = cbsclient.get('/service_component/scn_exists', + headers={"x-onap-requestid": 123456789}) + assert(json.loads(res.data) == {"foo3": "bar3"}) + assert(res.status_code == 200) + assert(res.headers["x-onap-requestid"] == "123456789") + + res = cbsclient.get('/service_component/scn_NOTexists', + headers={"x-onap-requestid": 123456789}) + assert(res.status_code == 404) + assert(res.headers["x-onap-requestid"] == "123456789") + + res = cbsclient.get('/service_component/asdfasdf', + headers={"x-onap-requestid": 123456789}) + assert(res.status_code == 500) + assert(res.headers["x-onap-requestid"] == "123456789") + + +def test_generic(monkeypatch, cbsclient, monkeyed_requests_get, monkeyed_requests_put): + monkeypatch.setattr('requests.put', monkeyed_requests_put) + monkeypatch.setattr('requests.get', monkeyed_requests_get) + + res = cbsclient.get('/dti/{0}'.format(TEST_NAME)) + assert json.loads(res.data) == {"my": "dti"} + assert res.json == {"my": "dti"} + assert res.status_code == 200 + assert "x-onap-requestid" in res.headers + + res = cbsclient.get('/nokeyforyou/{0}'.format(TEST_NAME)) + assert res.status_code == 404 + assert "x-onap-requestid" in res.headers + + res = cbsclient.get('/policies/{0}'.format(TEST_NAME)) + assert res.status_code == 400 + assert "x-onap-requestid" in res.headers + + +def test_resolve_all(monkeypatch, cbsclient, monkeyed_requests_put, monkeyed_get_connection_info_from_consul, expected_config): + monkeypatch.setattr('requests.put', monkeyed_requests_put) + monkeypatch.setattr('config_binding_service.client._get_connection_info_from_consul', monkeyed_get_connection_info_from_consul) + withstuff = {'config': {'my': 'amazing config'}, + 'dti': {'my': 'dti'}, + 'policies': {'items': [{'policyName': 'DCAE_alex.Config_MS_alex_microservice.132.xml', 'policyConfigMessage': 'Config Retrieved! ', 'responseAttributes': {}, 'policyConfigStatus': 'CONFIG_RETRIEVED', 'matchingConditions': {'ONAPName': 'DCAE', 'Name': 'DCAE', 'ConfigName': 'alex_config_name'}, 'config': {'policyScope': 'alex_policy_scope', 'configName': 'alex_config_name', 'description': 'test DCAE policy-handler', 'service': 'alex_service', 'policyName': 'alex_policy_name', 'riskLevel': '3', 'key1': 'value1', 'policy_hello': 'world!', 'content': {'foo': 'microservice3', 'foo_updated': '2018-01-30T13:25:33.222Z'}, 'riskType': '1712_ETE', 'guard': 'False', 'version': '0.0.1', 'location': 'Central', 'policy_updated_ts': '2018-02-19T15:09:55.217Z', 'updated_policy_id': 'DCAE_alex.Config_MS_alex_microservice', 'policy_updated_to_ver': '132', 'priority': '4', 'policy_updated_from_ver': '131', 'templateVersion': '2', 'uuid': '5e87d7c5-0daf-4b6b-ab92-5365cf5db1ef'}, 'property': None, 'type': 'JSON', 'policyVersion': '132'}, {'policyName': 'DCAE_alex.Config_db_client_policy_id_value.133.xml', 'policyConfigMessage': 'Config Retrieved! ', 'responseAttributes': {}, 'policyConfigStatus': 'CONFIG_RETRIEVED', 'matchingConditions': {'ONAPName': 'DCAE', 'Name': 'DCAE', 'ConfigName': 'alex_config_name'}, 'config': {'db_client_ts': '2017-11-21T12:12:13.696Z', 'db_client': 'ipsum', 'policy_hello': 'world!', 'policy_updated_from_ver': '132', 'updated_policy_id': 'DCAE_alex.Config_db_client_policy_id_value', 'policy_updated_ts': '2018-02-19T15:09:55.812Z', 'policy_updated_to_ver': '133'}, 'property': None, 'type': 'JSON', 'policyVersion': '133'}], 'event': {'action': 'gathered', 'timestamp': '2018-02-19 15:36:44.877380', 'update_id': 'bb73c20a-5ff8-450f-8223-da6720ade267', 'policies_count': 2}}} + + assert cbsclient.get('service_component_all/{0}'.format(TEST_NAME)).json == withstuff + + assert cbsclient.get('service_component_all/test_resolve_scn').json == {"config": expected_config} + + res = cbsclient.get('/service_component_all/{0}'.format(TEST_NAME)) + assert json.loads(res.data) == withstuff + assert res.json == withstuff + assert res.status_code == 200 + assert "x-onap-requestid" in res.headers + + res = cbsclient.get('/service_component_all/test_resolve_scn') + assert res.status_code == 200 + assert res.json == {"config": expected_config} + + res = cbsclient.get('/service_component_all/scn_NOTexists') + assert res.status_code == 404 + assert "x-onap-requestid" in res.headers diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..96c3467 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,206 @@ +# ============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========================================================= +# + +from config_binding_service import client +import pytest + + +# pytest doesnt support objects in conftest +class FakeReq(object): + """used to fake the logging params""" + def __init__(self): + self.path = "/unittest in {0}".format(__name__) + self.host = "localhost" + self.remote_addr = "6.6.6.6" + + +# pytest doesnt support objects in conftest +class FakeConnexion(object): + def __init__(self, headers, path, host, remote_addr): + self.headers = headers + self.path = path + self.host = host + self.remote_addr = remote_addr + + +def test_consul_get_all_as_transaction(monkeypatch, monkeyed_requests_put): + """tests _consul_get_all_as_transaction""" + monkeypatch.setattr('requests.put', monkeyed_requests_put) + allk = client._consul_get_all_as_transaction( + "test_service_component_name.unknown.unknown.unknown.dcae.onap.org", FakeReq(), "unit test xer") + assert allk == { + 'test_service_component_name.unknown.unknown.unknown.dcae.onap.org': {'my': 'amazing config'}, + 'test_service_component_name.unknown.unknown.unknown.dcae.onap.org:dti': {'my': 'dti'}, + 'test_service_component_name.unknown.unknown.unknown.dcae.onap.org:dmaap': {'foo': 'bar'}, + 'test_service_component_name.unknown.unknown.unknown.dcae.onap.org:policies/event': {'action': 'gathered', 'timestamp': '2018-02-19 15:36:44.877380', 'update_id': 'bb73c20a-5ff8-450f-8223-da6720ade267', 'policies_count': 2}, + 'test_service_component_name.unknown.unknown.unknown.dcae.onap.org:policies/items/DCAE_alex.Config_MS_alex_microservice': {'policyName': 'DCAE_alex.Config_MS_alex_microservice.132.xml', 'policyConfigMessage': 'Config Retrieved! ', 'responseAttributes': {}, 'policyConfigStatus': 'CONFIG_RETRIEVED', 'matchingConditions': {'ONAPName': 'DCAE', 'Name': 'DCAE', 'ConfigName': 'alex_config_name'}, 'config': {'policyScope': 'alex_policy_scope', 'configName': 'alex_config_name', 'description': 'test DCAE policy-handler', 'service': 'alex_service', 'policyName': 'alex_policy_name', 'riskLevel': '3', 'key1': 'value1', 'policy_hello': 'world!', 'content': {'foo': 'microservice3', 'foo_updated': '2018-01-30T13:25:33.222Z'}, 'riskType': '1712_ETE', 'guard': 'False', 'version': '0.0.1', 'location': 'Central', 'policy_updated_ts': '2018-02-19T15:09:55.217Z', 'updated_policy_id': 'DCAE_alex.Config_MS_alex_microservice', 'policy_updated_to_ver': '132', 'priority': '4', 'policy_updated_from_ver': '131', 'templateVersion': '2', 'uuid': '5e87d7c5-0daf-4b6b-ab92-5365cf5db1ef'}, 'property': None, 'type': 'JSON', 'policyVersion': '132'}, + 'test_service_component_name.unknown.unknown.unknown.dcae.onap.org:policies/items/DCAE_alex.Config_db_client_policy_id_value': {'policyName': 'DCAE_alex.Config_db_client_policy_id_value.133.xml', 'policyConfigMessage': 'Config Retrieved! ', 'responseAttributes': {}, 'policyConfigStatus': 'CONFIG_RETRIEVED', 'matchingConditions': {'ONAPName': 'DCAE', 'Name': 'DCAE', 'ConfigName': 'alex_config_name'}, 'config': {'db_client_ts': '2017-11-21T12:12:13.696Z', 'db_client': 'ipsum', 'policy_hello': 'world!', 'policy_updated_from_ver': '132', 'updated_policy_id': 'DCAE_alex.Config_db_client_policy_id_value', 'policy_updated_ts': '2018-02-19T15:09:55.812Z', 'policy_updated_to_ver': '133'}, 'property': None, 'type': 'JSON', 'policyVersion': '133'}, + 'test_service_component_name.unknown.unknown.unknown.dcae.onap.org:rels': ['my.amazing.relationship'] + } + + allk = client._consul_get_all_as_transaction("cbs_test_messed_up", FakeReq(), "unit test xer") + assert allk == {'cbs_test_messed_up': {'foo': 'bar'}, + 'cbs_test_messed_up:badkey': 'INVALID JSON'} + + +def test_get_config_rels_dmaap(monkeypatch, monkeyed_requests_put): + monkeypatch.setattr('requests.put', monkeyed_requests_put) + assert ({"foo3": "bar3"}, ["foo"], {"foo4": "bar4"}) == client._get_config_rels_dmaap("scn_exists", FakeReq(), "unit test xer") + assert ({"foo5": "bar5"}, [], {}) == client._get_config_rels_dmaap("scn_exists_nord", FakeReq(), "unit test xer") + + +def test_bad_config_http(): + test_config = {'yeahhhhh': "{{}}"} + test_rels = ["testing_bravo.somedomain.com"] + assert {'yeahhhhh': []} == client.resolve_override(test_config, test_rels) + + +def test_bad_config_dmaap(): + test_config = {'darkness': "<<>>"} + test_dmaap = {"WHO?": "darkness"} + assert {'darkness': {}} == client.resolve_override(test_config, test_dmaap) + + +def test_config_with_list(monkeypatch, monkeyed_get_connection_info_from_consul): + monkeypatch.setattr('config_binding_service.client._get_connection_info_from_consul', + monkeyed_get_connection_info_from_consul) + test_config_1 = {"dcae_target_type": [ + "vhss-ems", "pcrf-oam"], "downstream-laika": "{{ laika }}", "some-param": "Lorem ipsum dolor sit amet"} + test_rels_1 = ["3df5292249ae4a949f173063617cea8d_docker-snmp-polling-firstnet-m"] + test_bind_1 = client.resolve_override(test_config_1, test_rels_1, {}) + assert(test_bind_1 == {'dcae_target_type': [ + 'vhss-ems', 'pcrf-oam'], 'downstream-laika': [], 'some-param': 'Lorem ipsum dolor sit amet'}) + + test_config_2 = {"foo": ["{{cdap}}", "notouching", "<>"]} + test_rels_2 = ["cdap"] + test_dmaap_2 = {"yo": "im here"} + test_bind_2 = client.resolve_override(test_config_2, test_rels_2, test_dmaap_2) + assert(test_bind_2 == {"foo": [['666.666.666.666:666'], "notouching", "im here"]}) + + +def test_cdap(monkeypatch, monkeyed_get_connection_info_from_consul): + # user override to test CDAP functionality + monkeypatch.setattr('config_binding_service.client._get_connection_info_from_consul', + monkeyed_get_connection_info_from_consul) + test_rels = ["testing_alpha.somedomain.com", "testing_bravo.somedomain.com", + "testing_charlie.somedomain.com", "testing_charlie.somedomain.com", "cdap"] + test_config = {"streams_publishes": "{{alpha}}", + # should be dumped + "services_calls": [{"somekey": "{{charlie}}"}], + "cdap_to_manage": {'some_nested_thing': "{{cdap}}"}} # no dumps + test_bind_1 = client.resolve_override(test_config, test_rels) + assert test_bind_1 == {'services_calls': [{"somekey": ["5.5.5.5:555", "5.5.5.5:555"]}], 'streams_publishes': [ + "6.6.6.6:666"], 'cdap_to_manage': {'some_nested_thing': ['666.666.666.666:666']}} + assert test_bind_1['services_calls'] == [{"somekey": ["5.5.5.5:555", "5.5.5.5:555"]}] + assert test_bind_1['streams_publishes'] == ["6.6.6.6:666"] + + +def test_multiple_service_types(monkeypatch, monkeyed_get_connection_info_from_consul): + # test {{x,y,z}} + monkeypatch.setattr('config_binding_service.client._get_connection_info_from_consul', + monkeyed_get_connection_info_from_consul) + + # test 1: they all resovle + test_rels = ["testing_alpha.somedomain.com", "testing_bravo.somedomain.com", + "testing_charlie.somedomain.com", "testing_charlie.somedomain.com"] + config = {"ALL YOUR SERVICE BELONG TO US": "{{alpha,bravo,charlie}}"} + test_bind_1 = client.resolve_override(config, test_rels) + assert(test_bind_1 == {"ALL YOUR SERVICE BELONG TO US": [ + '6.6.6.6:666', '7.7.7.7:777', '5.5.5.5:555', '5.5.5.5:555']}) + + # test 2: two resolve, one is missing from rels key + config2 = {"two there one not exist": "{{alpha,bravo,notexist}}"} + test_bind_2 = client.resolve_override(config2, test_rels) + assert(test_bind_2 == {"two there one not exist": [ + '6.6.6.6:666', '7.7.7.7:777']}) + + # test 3: two resolve, one is in rels key but not registered + config3 = {"two there one unregistered": "{{alpha,bravo,unregistered}}"} + test_rels3 = ["testing_alpha.somedomain.com", + "testing_bravo.somedomain.com", "unregistered.somedomain.com"] + test_bind_3 = client.resolve_override(config3, test_rels3) + assert(test_bind_3 == {"two there one unregistered": [ + '6.6.6.6:666', '7.7.7.7:777']}) + + +def test_dmaap(monkeypatch): + # test resolving dmaap key + config = {"TODAY IS YOUR LUCKY DAY": "<>"} + # does not match + test_bind = client.resolve_override( + config, dmaap={"XX": "ABSOLVEME"}) # XX != XXX + assert(test_bind == {"TODAY IS YOUR LUCKY DAY": {}}) + # matches + test_bind_2 = client.resolve_override(config, dmaap={"XXX": "ABSOLVEME"}) + assert(test_bind_2 == {"TODAY IS YOUR LUCKY DAY": "ABSOLVEME"}) + + +def test_config(monkeypatch, monkeyed_get_connection_info_from_consul): + # test config override + monkeypatch.setattr('config_binding_service.client._get_connection_info_from_consul', + monkeyed_get_connection_info_from_consul) + test_config = {"autoderegisterafter": "10m", "cdap_to_manage": { + 'some_nested_thing': "{{cdap}}"}, "bindingttw": 5, "hcinterval": "5s"} + test_rels = ["cdap"] + test_bind_1 = client.resolve_override(test_config, test_rels) + assert test_bind_1 == {'autoderegisterafter': '10m', 'cdap_to_manage': { + 'some_nested_thing': ['666.666.666.666:666']}, 'bindingttw': 5, 'hcinterval': '5s'} + + +def test_non_existent(monkeypatch, monkeyed_get_connection_info_from_consul): + # test a valid config-rels but the key is not in Consul + monkeypatch.setattr('config_binding_service.client._get_connection_info_from_consul', + monkeyed_get_connection_info_from_consul) + test_config = {"you shall not be fufilled": "{{nonexistent_hope}}"} + # hopefully not registered in Consul.. + test_rels = ["nonexistent_hope.rework-central.ecomp.somedomain.com"] + test_bind_1 = client.resolve_override(test_config, test_rels, {}) + assert(test_bind_1 == {"you shall not be fufilled": []}) + + +def test_broker_redirect(monkeypatch, monkeyed_get_connection_info_from_consul): + # test the broker redirect + monkeypatch.setattr('config_binding_service.client._get_connection_info_from_consul', + monkeyed_get_connection_info_from_consul) + test_config = {"gimmie_dat_cdap": "{{cdap_serv}}"} + test_rels = ["cdap_serv.dcae.ecomp.somedomain.com"] + assert {"gimmie_dat_cdap": ['http://1.1.1.1:444/application/cdap_serv.dcae.ecomp.somedomain.com'] + } == client.resolve_override(test_config, test_rels) + + +def test_both(monkeypatch, monkeyed_get_connection_info_from_consul, expected_config): + # test rels and http + monkeypatch.setattr('config_binding_service.client._get_connection_info_from_consul', + monkeyed_get_connection_info_from_consul) + test_rels = ["testing_alpha.somedomain.com", "testing_bravo.somedomain.com", + "testing_charlie.somedomain.com", "testing_charlie.somedomain.com"] + test_dmaap = {"WHO?": "darkness"} + config = {"deep": {"ALL YOUR SERVICE BELONG TO US": "{{alpha,bravo,charlie}}"}, + "doubledeep": {"sodeep": {"hello": "<>"}}} + test_bind_1 = client.resolve_override(config, test_rels, test_dmaap) + assert(test_bind_1 == expected_config) + + +def test_failures(monkeypatch, monkeyed_requests_put, monkeyed_requests_get): + monkeypatch.setattr('requests.put', monkeyed_requests_put) + monkeypatch.setattr('requests.get', monkeyed_requests_get) + monkeypatch.setattr('connexion.request', FakeConnexion({"x-onap-requestid": 123456789}, "/service_component", "mytestingmachine", "myremoteclient")) + assert(client.resolve("scn_exists", FakeReq(), "unit test xer") == {"foo3": "bar3"}) + with pytest.raises(client.CantGetConfig): + client.resolve("scn_NOTexists", FakeReq(), "unit test xer") + with pytest.raises(client.CantGetConfig): + client.get_key("nokeyforyou", "test_service_component_name.unknown.unknown.unknown.dcae.onap.org", FakeReq(), "unit test xer") 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} -- cgit 1.2.3-korg