diff options
Diffstat (limited to 'config_binding_service')
-rw-r--r-- | config_binding_service/client.py | 117 | ||||
-rw-r--r-- | config_binding_service/controller.py | 39 | ||||
-rw-r--r-- | config_binding_service/swagger/swagger.yaml | 31 |
3 files changed, 137 insertions, 50 deletions
diff --git a/config_binding_service/client.py b/config_binding_service/client.py index 957b4be..8facf35 100644 --- a/config_binding_service/client.py +++ b/config_binding_service/client.py @@ -17,13 +17,13 @@ # ECOMP is a trademark and service mark of AT&T Intellectual Property. import re -import requests -import copy +from functools import partial, reduce import base64 +import copy import json +import requests 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() @@ -34,13 +34,31 @@ 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:* @@ -54,27 +72,26 @@ def _consul_get_all_as_transaction(service_component_name): } }] - response = requests.put("{0}/v1/txn".format(CONSUL), - json = payload) + response = requests.put("{0}/v1/txn".format(CONSUL), json=payload) try: response.raise_for_status() - except requests.exceptions.HTTPError as e: - raise CantGetConfig(e.response.status_code, e.response.text) + except requests.exceptions.HTTPError as exc: + raise CantGetConfig(exc.response.status_code, exc.response.text) - res = json.loads(response.text)['Results'] + result = json.loads(response.text)['Results'] new_res = {} - for r in res: - key = r["KV"]["Key"] - val = r["KV"]["Value"] - new_res[key] = json.loads(base64.b64decode(r["KV"]["Value"]).decode("utf-8")) + for res in result: + key = res["KV"]["Key"] + new_res[key] = json.loads(base64.b64decode(res["KV"]["Value"]).decode("utf-8")) 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] @@ -82,6 +99,7 @@ def _get_config_rels_dmaap(service_component_name): dmaap = allk.get(service_component_name + ":dmaap", {}) return config, rels, dmaap + def _get_connection_info_from_consul(service_component_name): """ Call consul's catalog @@ -97,19 +115,18 @@ def _get_connection_info_from_consul(service_component_name): 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) + 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 {0}".format(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): """ @@ -118,11 +135,12 @@ def _replace_rels_template(rels, template_identifier): 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) + 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))) + return list(filter(lambda x: x is not None, map(_get_connection_info_from_consul, returnl))) + def _replace_dmaap_template(dmaap, template_identifier): """ @@ -131,6 +149,7 @@ def _replace_dmaap_template(dmaap, template_identifier): """ 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. @@ -145,7 +164,7 @@ def _replace_value(v, rels, dmaap): 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(",")), []) + 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() @@ -159,6 +178,7 @@ def _replace_value(v, rels, dmaap): 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. @@ -169,19 +189,21 @@ def _recurse(config, rels, dmaap): """ if isinstance(config, list): return [_recurse(item, rels, dmaap) for item in config] - elif isinstance(config,dict): + if isinstance(config, dict): for key in config: config[key] = _recurse(config[key], rels, dmaap) return config - elif isinstance(config, six.string_types): + if isinstance(config, six.string_types): return _replace_value(config, rels, dmaap) - else: - #not a dict, not a list, not a string, nothing to do. - return config + #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 @@ -189,6 +211,7 @@ def resolve(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. @@ -197,9 +220,10 @@ def resolve_override(config, rels=[], dmaap={}): #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, DTI, and policies, and any other key (other than :dmaap and :rels) + 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 = {} @@ -219,11 +243,24 @@ def resolve_all(service_component_name): returnk["policies"]["event"] = allk[k] elif ":policies/items" in k: returnk["policies"]["items"].append(allk[k]) - elif k == service_component_name or k.endswith(":rels") or k.endswith(":dmaap"): - pass else: - suffix = k.split(":")[1] #this would blow up if you had a key in consul without a : but this shouldnt happen - returnk[suffix] = allk[k] + if not(k == service_component_name or k.endswith(":rels") or k.endswith(":dmaap")): + suffix = k.split(":")[1] #this would blow up if you had a key in consul without a : but this shouldnt happen + 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")) diff --git a/config_binding_service/controller.py b/config_binding_service/controller.py index 4ece194..dbdb57b 100644 --- a/config_binding_service/controller.py +++ b/config_binding_service/controller.py @@ -16,13 +16,14 @@ # # 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 +from flask import Response import json +from config_binding_service import client, get_consul_uri, get_logger _logger = get_logger(__name__) + def bind_all(service_component_name): try: allk = client.resolve_all(service_component_name) @@ -37,12 +38,13 @@ def bind_all(service_component_name): return Response(response="Unknown error: please report", status=500) + 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") + status=200, + mimetype="application/json") except client.CantGetConfig as e: return Response(status=e.code, response=e.response) @@ -51,14 +53,33 @@ def bind_config_for_scn(service_component_name): return Response(response="Please report this error", status=500) + +def get_key(key, service_component_name): + try: + bound = client.get_key(key, 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 client.BadRequest as exc: + return Response(status=exc.code, + response=exc.response, + mimetype="text/plain") + 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) + 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) - + 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 index ac7098f..cfe0944 100644 --- a/config_binding_service/swagger/swagger.yaml +++ b/config_binding_service/swagger/swagger.yaml @@ -20,7 +20,7 @@ --- swagger: "2.0" info: - version: "2.0.0" + version: "2.1.0" title: "Config Binding Service" paths: /service_component/{service_component_name}: @@ -59,6 +59,35 @@ paths: 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 + type: "string" + - name: "service_component_name" + in: "path" + description: "Service Component Name." + required: true + 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" + schema: + type: object + 404: + description: "key does not exist" + schema: + type: string + 400: + description: "bad request. Currently this is only returned on :policies, which is a complex object, and should be gotten through service_component_all" + 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" |