summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTommy Carpenter <tommy@research.att.com>2017-08-11 15:02:32 -0400
committerTommy Carpenter <tommy@research.att.com>2017-08-11 19:12:47 +0000
commit0581c1ed0320acd612dc38757744e8cc1212014b (patch)
tree7a9f4dbb689f56307c4ad1ca03d942c77d30b6b5
parent816ac43c7c53508ca3ec174f38bd49d1583f1d12 (diff)
Intial commit of CBS to ONAP
Change-Id: I2082544efc59476ac8de0dc39c899f968c3847bd Signed-off-by: Tommy Carpenter <tommy@research.att.com> Issue-Id: DCAEGEN2-47
-rw-r--r--.gitignore90
-rw-r--r--.gitreview4
-rw-r--r--Changelog.md31
-rw-r--r--Dockerfile14
-rw-r--r--LICENSE.txt31
-rw-r--r--README.md50
-rwxr-xr-xbin/run.py15
-rw-r--r--config_binding_service/__init__.py52
-rw-r--r--config_binding_service/client.py181
-rw-r--r--config_binding_service/controller.py49
-rw-r--r--config_binding_service/swagger/swagger.yaml33
-rw-r--r--doc/cbs_diagram.pngbin0 -> 72540 bytes
-rw-r--r--requirements.txt5
-rw-r--r--setup.py40
-rw-r--r--tests/test_binding.py139
-rw-r--r--tox.ini14
16 files changed, 748 insertions, 0 deletions
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 <<f1,f2>> 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
--- /dev/null
+++ b/doc/cbs_diagram.png
Binary files 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" : "<<XXX>>"}
+ #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" : "<<WHO?>>"}}
+ }
+ 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