diff options
Diffstat (limited to 'config_binding_service/client.py')
-rw-r--r-- | config_binding_service/client.py | 279 |
1 files changed, 0 insertions, 279 deletions
diff --git a/config_binding_service/client.py b/config_binding_service/client.py deleted file mode 100644 index a87c3bc..0000000 --- a/config_binding_service/client.py +++ /dev/null @@ -1,279 +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 LOGGER - - -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): - """ - 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, - } - }] - - response = requests.put("{0}/v1/txn".format(CONSUL), json=payload) - - 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): - allk = _consul_get_all_as_transaction(service_component_name) - 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 - - 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 %s", 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 %s registered in Consul!", service_component_name) - 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) - LOGGER.info("component is a CDAP application; trying the broker redirect on %s", redirectish_url) - 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 is not "": - 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 <<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): - """ - 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): - """ - Return the bound config of service_component_name - """ - config, rels, dmaap = _get_config_rels_dmaap(service_component_name) - 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): - """ - Return config, policies, and any other k such that service_component_name:k exists (other than :dmaap and :rels) - """ - allk = _consul_get_all_as_transaction(service_component_name) - returnk = {} - - # replace the config with the resolved config - returnk["config"] = resolve(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): - """ - Try to fetch a key k from Consul of the form service_component_name:k - """ - if key == "policies": - raise BadRequest( - ":policies is a complex folder and should be retrieved using the service_component_all API") - response = requests.get( - "{0}/v1/kv/{1}:{2}".format(CONSUL, service_component_name, key)) - 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")) |