summaryrefslogtreecommitdiffstats
path: root/config_binding_service/client.py
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 /config_binding_service/client.py
parent816ac43c7c53508ca3ec174f38bd49d1583f1d12 (diff)
Intial commit of CBS to ONAP
Change-Id: I2082544efc59476ac8de0dc39c899f968c3847bd Signed-off-by: Tommy Carpenter <tommy@research.att.com> Issue-Id: DCAEGEN2-47
Diffstat (limited to 'config_binding_service/client.py')
-rw-r--r--config_binding_service/client.py181
1 files changed, 181 insertions, 0 deletions
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)