From 0581c1ed0320acd612dc38757744e8cc1212014b Mon Sep 17 00:00:00 2001 From: Tommy Carpenter Date: Fri, 11 Aug 2017 15:02:32 -0400 Subject: Intial commit of CBS to ONAP Change-Id: I2082544efc59476ac8de0dc39c899f968c3847bd Signed-off-by: Tommy Carpenter Issue-Id: DCAEGEN2-47 --- .gitignore | 90 ++++++++++++++ .gitreview | 4 + Changelog.md | 31 +++++ Dockerfile | 14 +++ LICENSE.txt | 31 +++++ README.md | 50 ++++++++ bin/run.py | 15 +++ config_binding_service/__init__.py | 52 ++++++++ config_binding_service/client.py | 181 ++++++++++++++++++++++++++++ config_binding_service/controller.py | 49 ++++++++ config_binding_service/swagger/swagger.yaml | 33 +++++ doc/cbs_diagram.png | Bin 0 -> 72540 bytes requirements.txt | 5 + setup.py | 40 ++++++ tests/test_binding.py | 139 +++++++++++++++++++++ tox.ini | 14 +++ 16 files changed, 748 insertions(+) create mode 100644 .gitignore create mode 100644 .gitreview create mode 100644 Changelog.md create mode 100644 Dockerfile create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100755 bin/run.py 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/swagger/swagger.yaml create mode 100644 doc/cbs_diagram.png create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 tests/test_binding.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e346599 --- /dev/null +++ b/.gitignore @@ -0,0 +1,90 @@ +.DS_Store +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject diff --git a/.gitreview b/.gitreview new file mode 100644 index 0000000..ebcddc1 --- /dev/null +++ b/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=gerrit.onap.org +port=29418 +project=dcaegen2/platform/configbinding.git diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..9985855 --- /dev/null +++ b/Changelog.md @@ -0,0 +1,31 @@ +# Change Log +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/). + +## [1.2.0] +* Remove waterfalled CONSUL_HOST +* Add ONAP liscenses +* Remove references to specific telco and it's IPs in tests + +## [1.1.0] +* Add a healthcheck endpoint +* Fix a bug where a 404 config not found was being returned as a 500 + +## [1.0.1] +* Fix {{}} to resolve to [] instead of whatever is in rels key +* Remove all impure tests. All tests are now unit tests. + +## [1.0.0] +* GLORIOUS CHANGE! At some point, CASK fixed a bug where if you sent a configuration JSON to CDAP that contained a value that was not a string, it would blow up. This allows me to remove the endpoint specific to CDAP components so the same endpoint is now used for Docker and CDAP. +* Props to Terry Troutman for helping me discover this. +* Removes some impure tests. Still some impurity there + +## [0.9.0] +* In addition to the "rels key" a new key was introduced, the "dmaap key". Support replacing dmaap keys assumung the tempalating language "<< >>" + +## [0.8.0] +* Start changelog.. +* Fix a 500 bug where the CBS would return a 500 when a service was in a rels key but that service was not registered in Consul +* Support a new feature where you can now bind {{x,y,....}} instead of just {{x}}. The return list is all concat together diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f9e4981 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.5 +MAINTAINER tommy@research.att.com + +ADD . /tmp + +#need pip > 8 to have internal pypi repo in requirements.txt +RUN pip install --upgrade pip +#do the install +WORKDIR /tmp +RUN pip install -e . + +EXPOSE 10000 + +CMD run.py diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..7c1e63a --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,31 @@ +============LICENSE_START======================================================= +org.onap.dcae +================================================================================ +Copyright (c) 2017 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. + +Copyright (c) 2017 AT&T Intellectual Property. All rights reserved. +=================================================================== +Licensed under the Creative Commons License, Attribution 4.0 Intl. (the "License"); +you may not use this documentation except in compliance with the License. +You may obtain a copy of the License at + https://creativecommons.org/licenses/by/4.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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..04bc382 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# config_binding_service + +# Interface Diagram +This repo is the thing in red: + +![Alt text](doc/cbs_diagram.png?raw=true) + +# Overview + +DCAE has a "templating language" built into components' configurations, as explained further below. +The orchestrator populates one/two keys (depending on the blueprint) into Consul that are used to *bind* component configurations config, a "rels key" and a "dmaap key". +If component A wants to connect to a component of type B, then A's rels key holds what specific service component name of B that A should connect to over direct HTTP. +Service component name here means the full name that the component of type B is registered under in Consul (there can be multiple components of type B registered in Consul). +The CBS (config binding service) then pulls down that rels key, fetches the connection information about that B (IP:Port), and replaces it into A's config. +There is also a "dmaap key", which is the same concept, except what gets injected is a JSON of DMaaP connection information instead of an IP:Port. + +# Usage +hit `url_of_this/service_component/service_component_name` and you are returned your bound config. + +(Note: there is also a backdoor in the `client` module that allows you to pass in a direct JSON and a direct rels, but this isn't exposed via the HTTP API as of now) + +# 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. +2. `service_component_name` is in consul as a key and holds the config +3. `service_component_name:rel` is in consul as a key *if* you are expecting a direct HTTP resolution, and holds the service component names of connections. +4. `service_component_name:dmaap` is in consul *if* you are expecting a DMaaP resolution, and holds the components DMaaP information. + +# Templating Language +The CBS tries to resolve a component's configuration with a templating language. We have two templating languages embedded in our component's configuration (`{{...}}` and `<<...>>`). There are two because the CBS has to be able to distinguish between a rels-key-resolve and a dmaap-key-resolve. That is, if component X is trying to bind their component, and they want to talk to Y, someone has to tell the CBS whether they are trying to talk via IP:port or a feed. + +Specifically, if the CBS sees: + +``` +X's configuration: +{ + ... + config_key : << F >> // will try to resolve via X:dmaap and look for F + config_key : {{ F }} // will try to resolve via X:rels and look for F +} +``` + +# Tests +Run: +``` +set -x CONSUL_HOST "your_consul_dns_name.somedomain.com"; set -x HOSTNAME "config_binding_service" +cd tests/ +pytest +``` + + diff --git a/bin/run.py b/bin/run.py new file mode 100755 index 0000000..7d30a00 --- /dev/null +++ b/bin/run.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +import connexion +import sys +from config_binding_service import get_logger + +_logger = get_logger(__name__) + +if __name__ == '__main__': + try: + app = connexion.App(__name__, specification_dir='../config_binding_service/swagger/') + app.add_api('swagger.yaml', arguments={'title': 'Config Binding Service'}) + app.run(host='0.0.0.0', port=10000, debug=False) + except Exception as e: + _logger.error("Fatal error. Could not start webserver due to: {0}".format(e)) diff --git a/config_binding_service/__init__.py b/config_binding_service/__init__.py new file mode 100644 index 0000000..51d3246 --- /dev/null +++ b/config_binding_service/__init__.py @@ -0,0 +1,52 @@ +# ============LICENSE_START======================================================= +# org.onap.dcae +# ================================================================================ +# Copyright (c) 2017 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 logging + +'''Configures the module root logger''' +root = logging.getLogger() +if root.handlers: + root.handlers.clear() +formatter = logging.Formatter('%(asctime)s | %(name)s | %(module)s | %(funcName)s | %(lineno)d | %(levelname)s | %(message)s') +handler = logging.StreamHandler() +handler.setFormatter(formatter) +root.addHandler(handler) +root.setLevel("DEBUG") + +class BadEnviornmentENVNotFound(Exception): + pass + +def get_logger(module=None): + '''Returns a module-specific logger or global logger if the module is None''' + return root if module is None else root.getChild(module) + +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") + diff --git a/config_binding_service/client.py b/config_binding_service/client.py new file mode 100644 index 0000000..02354ee --- /dev/null +++ b/config_binding_service/client.py @@ -0,0 +1,181 @@ +# ============LICENSE_START======================================================= +# org.onap.dcae +# ================================================================================ +# Copyright (c) 2017 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 +import requests +import copy +import base64 +import json +import six +from config_binding_service import get_consul_uri, get_logger +from functools import partial, reduce + +_logger = get_logger(__name__) +CONSUL = get_consul_uri() + +template_match_rels = re.compile("\{{2}([^\}\{]*)\}{2}") +template_match_dmaap = re.compile("<{2}([^><]*)>{2}") + +### +# Cusom Exception +### +class CantGetConfig(Exception): + def __init__(self, code, response): + self.code = code + self.response = response +### +# Private Functions +### +def _consul_get_key(key): + """ + Try to fetch a key from Consul. + No error checking here, let caller deal with it + """ + _logger.info("Fetching {0}".format(key)) + response = requests.get("{0}/v1/kv/{1}".format(CONSUL, key)) + response.raise_for_status() + D = json.loads(response.text)[0] + return json.loads(base64.b64decode(D["Value"]).decode("utf-8")) + +def _get_config_rels_dmaap(service_component_name): + try: + config = _consul_get_key(service_component_name) #not ok if no config + except requests.exceptions.HTTPError as e: + #might be a 404, or could be not even able to reach consul (503?), bubble up the requests error + raise CantGetConfig(e.response.status_code, e.response.text) + + rels = [] + dmaap = {} + try: #Not all nodes have relationships, so catch the error here and return [] if so + rels = _consul_get_key("{0}:rel".format(service_component_name)) + except requests.exceptions.HTTPError: #ok if no rels key, might just have dmaap key + pass + try: + dmaap = _consul_get_key("{0}:dmaap".format(service_component_name)) + except requests.exceptions.HTTPError: #ok if no dmaap key + pass + 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 + + TODO: WARNING: FIXTHIS: CALLINTHENATIONALARMY: + This tries to determine that a service_component_name is a cdap application by inspecting service_component_name and name munging. However, this would force all CDAP applications to have cdap_app in their name. A much better way to do this is to do some kind of catalog_lookup here, OR MAYBE change this API so that the component_type is passed in somehow. THis is a gaping TODO. + """ + _logger.info("Retrieving connection information for {0}".format(service_component_name)) + res = requests.get("{0}/v1/catalog/service/{1}".format(CONSUL, service_component_name)) + res.raise_for_status() + services = res.json() + if services == []: + _logger.info("Warning: config and rels keys were both valid, but there is no component named {0} registered in Consul!".format(service_component_name)) + return None #later will get filtered out + else: + ip = services[0]["ServiceAddress"] + port = services[0]["ServicePort"] + if "cdap_app" in service_component_name: + redirectish_url = "http://{0}:{1}/application/{2}".format(ip, port, service_component_name) + _logger.info("component is a CDAP application; trying the broker redirect on {0}".format(redirectish_url)) + r = requests.get(redirectish_url) + r.raise_for_status() + details = r.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"] } + else: + return "{0}:{1}".format(ip, 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 r in rels: + if template_identifier in r and template_identifier is not "": + returnl.append(r) + #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: + template_identifier = match_on_rels.groups()[0].strip() #now holds just x,.. of {{x,...}} + 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): + for key in config: + v = config[key] + if isinstance(v, list): + replacement = [_recurse(item, rels, dmaap) for item in v] + elif isinstance(v,dict): + replacement = _recurse(v, rels, dmaap) + else: + replacement = _replace_value(config[key], rels, dmaap) + config[key] = replacement + return config + +######### +# PUBLIC API +######### +def resolve(service_component_name): + """ + Return the bound config of service_component_name + """ + config, rels, dmaap = _get_config_rels_dmaap(service_component_name) + _logger.info("Fetching {0}: config={1}, rels={2}".format(service_component_name, json.dumps(config), rels)) + 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) diff --git a/config_binding_service/controller.py b/config_binding_service/controller.py new file mode 100644 index 0000000..a74d60f --- /dev/null +++ b/config_binding_service/controller.py @@ -0,0 +1,49 @@ +# ============LICENSE_START======================================================= +# org.onap.dcae +# ================================================================================ +# Copyright (c) 2017 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 import client, get_consul_uri, get_logger +import requests +from flask import request, Response +import json + +def bind_config_for_scn(service_component_name): + try: + bound = client.resolve(service_component_name) + return Response(response=json.dumps(bound), + status=200, + mimetype="application/json") + except client.CantGetConfig as e: + return Response(status=e.code, + response=e.response) + except Exception as e: #should never happen... + _logger.error(e) + return Response(response="Please report this error", + status=500) + +def healthcheck(): + #got this far, I must be alive... check my connection to Consul by checking myself + CONSUL = get_consul_uri() + res = requests.get("{0}/v1/catalog/service/config_binding_service".format(CONSUL)) + if res.status_code == 200: + return Response(response = "CBS is alive and Consul connection OK", + status = 200) + else: + return Response(response = "CBS is alive but cannot reach Consul", + status = 503) + diff --git a/config_binding_service/swagger/swagger.yaml b/config_binding_service/swagger/swagger.yaml new file mode 100644 index 0000000..208e441 --- /dev/null +++ b/config_binding_service/swagger/swagger.yaml @@ -0,0 +1,33 @@ +--- +swagger: "2.0" +info: + version: "1.0.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 and service_component_name:rels must be keys in consul." + required: true + 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 + schema: + type: object + 404: + description: there is no configuration in Consul for this component + /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" + parameters: [] + responses: + 200: + description: Successful response + 503: + description: the config binding service cannot reach Consul diff --git a/doc/cbs_diagram.png b/doc/cbs_diagram.png new file mode 100644 index 0000000..67287d0 Binary files /dev/null and b/doc/cbs_diagram.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1863f86 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +Flask==0.12.2 +connexion==1.1.12 +requests==2.18.2 +pytest==3.0.3 +six==1.10.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..cc28a43 --- /dev/null +++ b/setup.py @@ -0,0 +1,40 @@ +# ============LICENSE_START======================================================= +# org.onap.dcae +# ================================================================================ +# Copyright (c) 2017 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 setuptools import setup, find_packages +from pip.req import parse_requirements +from pip.download import PipSession + +import pip +pip.main(['install','-r','requirements.txt']) + +setup( + name='config_binding_service', + version='1.2.0', + packages=find_packages(), + author = "Tommy Carpenter", + author_email = "tommy at research dot a t t dot com", + description='Service to fetch and bind configurations', + license = "", + keywords = "", + url = "ONAP URL TBD", + zip_safe=False, + scripts = ["bin/run.py"] +) diff --git a/tests/test_binding.py b/tests/test_binding.py new file mode 100644 index 0000000..3bbe5d9 --- /dev/null +++ b/tests/test_binding.py @@ -0,0 +1,139 @@ +# ============LICENSE_START======================================================= +# org.onap.dcae +# ================================================================================ +# Copyright (c) 2017 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 import client +import pytest +import json + +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) + +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(monkeypatch): + #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): + #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}}"} + test_rels = ["nonexistent_hope.rework-central.ecomp.somedomain.com"] #hopefully not registered in Consul.. + test_bind_1 = client.resolve_override(test_config, test_rels, {}) + assert(test_bind_1 == {"you shall not be fufilled" : []}) + +def test_cdap(monkeypatch): + #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}}", + "services_calls" : [{"somekey" : "{{charlie}}"}], #should be dumped + "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_broker_redirect(monkeypatch): + #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_multiple_service_types(monkeypatch): + #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_both(monkeypatch): + #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) + expected_config = { + "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"}} + } + assert(test_bind_1 == expected_config) + + diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..da487f2 --- /dev/null +++ b/tox.ini @@ -0,0 +1,14 @@ +# content of: tox.ini , put in same dir as setup.py +[tox] +envlist = py27,py35 +[testenv] +deps= + Flask + connexion + pytest + requests + six +setenv = + CONSUL_HOST = 8.8.8.8 + HOSTNAME = config_binding_service +commands=pytest -- cgit 1.2.3-korg