diff options
author | Tommy Carpenter <tommy@research.att.com> | 2019-05-29 13:36:01 -0400 |
---|---|---|
committer | Tommy Carpenter <tommy@research.att.com> | 2019-06-04 09:12:25 -0400 |
commit | e14b49ead38227ff17d760c4771d58d9c6d2e7c0 (patch) | |
tree | 9e3cdc16376a5fb5f4b825a3930b28a89f58bccd /config_binding_service | |
parent | 040d03d77587ce24f0e99ee504b5b0ff5473a39e (diff) |
Switch to gevent
Issue-ID: DCAEGEN2-1549
Change-Id: I762d9630f857a23b6ae61992d483cdca7bb6f88d
Signed-off-by: Tommy Carpenter <tommy@research.att.com>
Diffstat (limited to 'config_binding_service')
-rw-r--r-- | config_binding_service/__init__.py | 46 | ||||
-rw-r--r-- | config_binding_service/client.py | 297 | ||||
-rw-r--r-- | config_binding_service/controller.py | 108 | ||||
-rw-r--r-- | config_binding_service/logging.py | 237 | ||||
-rw-r--r-- | config_binding_service/openapi.yaml | 112 | ||||
-rwxr-xr-x | config_binding_service/run.py | 32 |
6 files changed, 832 insertions, 0 deletions
diff --git a/config_binding_service/__init__.py b/config_binding_service/__init__.py new file mode 100644 index 0000000..306a762 --- /dev/null +++ b/config_binding_service/__init__.py @@ -0,0 +1,46 @@ +# ============LICENSE_START======================================================= +# Copyright (c) 2017-2019 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 connexion + + +class BadEnviornmentENVNotFound(Exception): + """ + Specific exception to be raised when a required ENV varaible is missing + """ + pass + + +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") + + +# this has to be here due to circular dependency +app = connexion.App(__name__, specification_dir='.') +app.add_api('openapi.yaml', arguments={'title': 'Config Binding Service'}) diff --git a/config_binding_service/client.py b/config_binding_service/client.py new file mode 100644 index 0000000..c6a6753 --- /dev/null +++ b/config_binding_service/client.py @@ -0,0 +1,297 @@ +# ============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 utc, metrics + + +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, raw_request, xer): + """ + 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, + } + }] + + bts = utc() + response = requests.put("{0}/v1/txn".format(CONSUL), json=payload) + metrics(raw_request, bts, xer, "Consul", "/v1/txn".format(service_component_name), response.status_code, __name__, msg="Retrieving Consul transaction for all keys for {0}".format(service_component_name)) + + 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, raw_request, xer): + allk = _consul_get_all_as_transaction(service_component_name, raw_request, xer) + 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 + + DEPRECATION NOTE: + This function existed when DCAE was using Consul to resolve service component's connection information. + This relied on a "rels" key and a Cloudify relationship plugin to set up the magic. + The consensous is that this feature is no longer used. + This functionality is very likely deprecated by Kubernetes service discovery mechanism, and DMaaP. + + This function also includes logic related to CDAP, which is also likely deprecated. + + This code shall remain here for now but is at risk of being deleted in a future release. + """ + # Note: there should be a metrics log here, but see the deprecation note above; this function is due to be deleted. + res = requests.get("{0}/v1/catalog/service/{1}".format(CONSUL, service_component_name)) + res.raise_for_status() + services = res.json() + if services == []: + 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) + 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 != "": + 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, raw_request, xer): + """ + Return the bound config of service_component_name + + raw_request and xer are needed to form the correct metrics log + """ + config, rels, dmaap = _get_config_rels_dmaap(service_component_name, raw_request, xer) + 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, raw_request, xer): + """ + Return config, policies, and any other k such that service_component_name:k exists (other than :dmaap and :rels) + + raw_request and xer are needed to form the correct metrics log + """ + allk = _consul_get_all_as_transaction(service_component_name, raw_request, xer) + returnk = {} + + # replace the config with the resolved config + returnk["config"] = resolve_override(allk[service_component_name], + allk.get("{0}:rels".format(service_component_name), []), + allk.get("{0}:dmaap".format(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, raw_request, xer): + """ + Try to fetch a key k from Consul of the form service_component_name:k + + raw_request and xer are needed to form the correct metrics log + """ + if key == "policies": + raise BadRequest( + ":policies is a complex folder and should be retrieved using the service_component_all API") + + bts = utc() + path = "v1/kv/{0}:{1}".format(service_component_name, key) + response = requests.get("{0}/{1}".format(CONSUL, path)) + metrics(raw_request, bts, xer, "Consul", path, response.status_code, __name__, msg="Retrieving single Consul key {0} for {1}".format(key, service_component_name)) + + 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 new file mode 100644 index 0000000..c2eb21c --- /dev/null +++ b/config_binding_service/controller.py @@ -0,0 +1,108 @@ +# ============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 json +import requests +import connexion +import uuid +from flask import Response +from config_binding_service import client, get_consul_uri +from config_binding_service.logging import audit, utc, error, metrics + + +def _get_helper(json_expecting_func, **kwargs): + """ + Helper function used by several functions below + """ + try: + payload = json_expecting_func(**kwargs) + response, status_code, mimetype = json.dumps(payload), 200, "application/json" + except client.BadRequest as exc: + response, status_code, mimetype = exc.response, exc.code, "text/plain" + except client.CantGetConfig as exc: + response, status_code, mimetype = exc.response, exc.code, "text/plain" + except Exception: + response, status_code, mimetype = "Unknown error", 500, "text/plain" + return response, status_code, mimetype + + +def _get_or_generate_xer(raw_request): + """get or generate the transaction id""" + xer = raw_request.headers.get("x-onap-requestid", None) + if xer is None: + # some components are still using the old name + xer = raw_request.headers.get("x-ecomp-requestid", None) + if xer is None: + # the user did NOT supply a request id, generate one + xer = str(uuid.uuid4()) + return xer + + +def bind_all(service_component_name): + """ + Get all the keys in Consul for this SCN, and bind the config + """ + xer = _get_or_generate_xer(connexion.request) + bts = utc() + response, status_code, mimetype = _get_helper(client.resolve_all, service_component_name=service_component_name, raw_request=connexion.request, xer=xer) + audit(connexion.request, bts, xer, status_code, __name__, "called for component {0}".format(service_component_name)) + # Even though some older components might be using the ecomp name, we return the proper one + return Response(response=response, status=status_code, mimetype=mimetype, headers={"x-onap-requestid": xer}) + + +def bind_config_for_scn(service_component_name): + """ + Bind just the config for this SCN + """ + xer = _get_or_generate_xer(connexion.request) + bts = utc() + response, status_code, mimetype = _get_helper(client.resolve, service_component_name=service_component_name, raw_request=connexion.request, xer=xer) + audit(connexion.request, bts, xer, status_code, __name__, "called for component {0}".format(service_component_name)) + return Response(response=response, status=status_code, mimetype=mimetype, headers={"x-onap-requestid": xer}) + + +def get_key(key, service_component_name): + """ + Get a single key k of the form service_component_name:k from Consul. + Should not be used and will return a BAD REQUEST for k=policies because it's a complex object + """ + xer = _get_or_generate_xer(connexion.request) + bts = utc() + response, status_code, mimetype = _get_helper(client.get_key, key=key, service_component_name=service_component_name, raw_request=connexion.request, xer=xer) + audit(connexion.request, bts, xer, status_code, __name__, "called for component {0}".format(service_component_name)) + return Response(response=response, status=status_code, mimetype=mimetype, headers={"x-onap-requestid": xer}) + + +def healthcheck(): + """ + CBS Healthcheck + """ + xer = _get_or_generate_xer(connexion.request) + path = "v1/catalog/service/config_binding_service" + bts = utc() + res = requests.get("{0}/{1}".format(get_consul_uri(), path)) + status = res.status_code + if status == 200: + msg = "CBS is alive and Consul connection OK" + else: + msg = "CBS is alive but cannot reach Consul" + # treating this as a WARN because this could be a temporary network glitch. Also per EELF guidelines this is a 200 ecode (availability) + error(connexion.request, xer, "WARN", 200, tgt_entity="Consul", tgt_path="/v1/catalog/service/config_binding_service", msg=msg) + metrics(connexion.request, bts, xer, "Consul", path, res.status_code, __name__, msg="Checking Consul connectivity during CBS healthcheck, {0}".format(msg)) + audit(connexion.request, bts, xer, status, __name__, msg=msg) + return Response(response=msg, status=status) diff --git a/config_binding_service/logging.py b/config_binding_service/logging.py new file mode 100644 index 0000000..35750f2 --- /dev/null +++ b/config_binding_service/logging.py @@ -0,0 +1,237 @@ +# ============LICENSE_START======================================================= +# Copyright (c) 2017-2019 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 logging +from logging.handlers import RotatingFileHandler +from os import makedirs +import datetime + +# These loggers will be overwritten with EELF logging when running in Docker +_AUDIT_LOGGER = logging.getLogger("defaultlogger") +_ERROR_LOGGER = logging.getLogger("defaultlogger") +_METRICS_LOGGER = logging.getLogger("defaultlogger") + +# Set up debug logger +DEBUG_LOGGER = logging.getLogger("defaultlogger") +handler = logging.StreamHandler() +formatter = logging.Formatter("%(asctime)s [%(name)-12s] %(levelname)-8s %(message)s") +handler.setFormatter(formatter) +DEBUG_LOGGER.addHandler(handler) +DEBUG_LOGGER.setLevel(logging.DEBUG) + + +def _create_logger(name, logfile): + """ + Create a RotatingFileHandler and a streamhandler for stdout + https://docs.python.org/3/library/logging.handlers.html + what's with the non-pythonic naming in these stdlib methods? Shameful. + """ + logger = logging.getLogger(name) + file_handler = RotatingFileHandler(logfile, maxBytes=10000000, backupCount=2) # 10 meg with one backup.. + formatter = logging.Formatter("%(message)s") + file_handler.setFormatter(formatter) + logger.setLevel("DEBUG") + logger.addHandler(file_handler) + return logger + + +# Public + + +def get_module_logger(mod_name): + """ + To use this, do logger = get_module_logger(__name__) + """ + logger = logging.getLogger(mod_name) + handler = logging.StreamHandler() + formatter = logging.Formatter("%(asctime)s [%(name)-12s] %(levelname)-8s %(message)s") + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(logging.DEBUG) + return logger + + +def create_loggers(): + """ + Public method to set the global logger, launched from Run + This is *not* launched during unit testing, so unit tests do not create/write log files + """ + makedirs("/opt/logs", exist_ok=True) + + # create the audit log + aud_file = "/opt/logs/audit.log" + open(aud_file, "a").close() # this is like "touch" + global _AUDIT_LOGGER + _AUDIT_LOGGER = _create_logger("config_binding_service_audit", aud_file) + + # create the error log + err_file = "/opt/logs/error.log" + open(err_file, "a").close() # this is like "touch" + global _ERROR_LOGGER + _ERROR_LOGGER = _create_logger("config_binding_service_error", err_file) + + # create the metrics log + met_file = "/opt/logs/metrics.log" + open(met_file, "a").close() # this is like "touch" + global _METRICS_LOGGER + _METRICS_LOGGER = _create_logger("config_binding_service_metrics", met_file) + + +def utc(): + """gets current time in utc""" + return datetime.datetime.utcnow() + + +def audit(raw_request, bts, xer, rcode, calling_mod, msg="n/a"): + """ + write an EELF audit record per https://wiki.onap.org/download/attachments/1015849/ONAP%20application%20logging%20guidelines.pdf?api=v2 + %The audit fields implemented: + + 1 BeginTimestamp Implemented (bts) + 2 EndTimestamp Auto Injected when this is called + 3 RequestID Implemented (xer) + 5 threadId n/a + 7 serviceName Implemented (from Req) + 9 StatusCode Auto injected based on rcode + 10 ResponseCode Implemented (rcode) + 13 Category log level - all audit records are INFO. + 15 Server IP address Implemented (from Req) + 16 ElapsedTime Auto Injected (milliseconds) + 17 Server This is running in a Docker container so this is not applicable, my HOSTNAME is always "config_binding_service" + 18 ClientIPaddress Implemented (from Req) + 19 class name Implemented (mod), though docs say OOP, I am using the python module here + 20 Unused ...implemented.... + 21-25 Custom n/a + 26 detailMessage Implemented (msg) + + Not implemented + 4 serviceInstanceID - ? + 6 physical/virtual server name (Optional) + 8 PartnerName - nothing in the request tells me this + 11 Response Description - the CBS follows standard HTTP error codes so look them up + 12 instanceUUID - Optional + 14 Severity (Optional) + """ + ets = utc() + + _AUDIT_LOGGER.info( + "{bts}|{ets}|{xer}||n/a||{path}||{status}|{rcode}|||INFO||{servip}|{et}|config_binding_service|{clientip}|{calling_mod}|||||||{msg}".format( + bts=bts.isoformat(), + ets=ets.isoformat(), + xer=xer, + rcode=rcode, + path=raw_request.path.split("/")[1], + status="COMPLETE" if rcode < 400 else "ERROR", + servip=raw_request.host.split(":")[0], + et=int((ets - bts).microseconds / 1000), # supposed to be in milleseconds + clientip=raw_request.remote_addr, + calling_mod=calling_mod, + msg=msg, + ) + ) + + +def error(raw_request, xer, severity, ecode, tgt_entity="n/a", tgt_path="n/a", msg="n/a", adv_msg="n/a"): + """ + write an EELF error record per + the error fields implemented: + + 1 Timestamp Auto Injected when this is called + 2 RequestID Implemented (xer) + 3 ThreadID n/a + 4 ServiceName Implemented (from Req) + 6 TargetEntity Implemented (tgt_entity) + 7 TargetServiceName Implemented (tgt_path)/ + 8 ErrorCategory Implemented (severity) + 9. ErrorCode Implemented (ecode) + 10 ErrorDescription Implemented (msg) + 11. detailMessage Implemented (adv_msg) + + Not implemented: + 5 PartnerName - nothing in the request tells me this + """ + ets = utc() + + _ERROR_LOGGER.error( + "{ets}|{xer}|n/a|{path}||{tge}|{tgp}|{sev}|{ecode}|{msg}|{amsg}".format( + ets=ets, + xer=xer, + path=raw_request.path.split("/")[1], + tge=tgt_entity, + tgp=tgt_path, + sev=severity, + ecode=ecode, + msg=msg, + amsg=adv_msg, + ) + ) + + +def metrics(raw_request, bts, xer, target, target_path, rcode, calling_mod, msg="n/a"): + """ + write an EELF metrics record per https://wiki.onap.org/download/attachments/1015849/ONAP%20application%20logging%20guidelines.pdf?api=v2 + %The metrics fields implemented: + + 1 BeginTimestamp Implemented (bts) + 2 EndTimestamp Auto Injected when this is called + 3 RequestID Implemented (xer) + 5 threadId n/a + 7 serviceName Implemented (from Req) + 9 TargetEntity Implemented (target) + 10 TargetServiceName Implemented (target_path) + 11 StatusCode Implemented (based on rcode) + 12 Response Code Implemented (rcode) + 15 Category log level all metrics records are INFO. + 17 Server IP address Implemented (from Req) + 18 ElapsedTime Auto Injected (milliseconds) + 19 Server This is running in a Docker container so this is not applicable, my HOSTNAME is always "config_binding_service" + 20 ClientIPaddress Implemented (from Req) + 21 class name Implemented (mod), though docs say OOP, I am using the python module here + 22 Unused ...implemented.... + 24 TargetVirtualEntity n/a + 25-28 Custom n/a + 29 detailMessage Implemented (msg) + + Not implemented + 4 serviceInstanceID - ? + 6 physical/virtual server name (Optional) + 8 PartnerName - nothing in the request tells me this + 13 Response Description - the CBS follows standard HTTP error codes so look them up + 14 instanceUUID - Optional + 16 Severity (Optional) + 23 ProcessKey - optional + """ + ets = utc() + + _METRICS_LOGGER.info( + "{bts}|{ets}|{xer}||n/a||{path}||{tge}|{tgp}|{status}|{rcode}|||INFO||{servip}|{et}|config_binding_service|{clientip}|{calling_mod}|||n/a|||||{msg}".format( + bts=bts.isoformat(), + ets=ets.isoformat(), + xer=xer, + path=raw_request.path.split("/")[1], + tge=target, + tgp=target_path, + status="COMPLETE" if rcode < 400 else "ERROR", + rcode=rcode, + servip=raw_request.host.split(":")[0], + et=int((ets - bts).microseconds / 1000), # supposed to be in milleseconds + clientip=raw_request.remote_addr, + calling_mod=calling_mod, + msg=msg, + ) + ) diff --git a/config_binding_service/openapi.yaml b/config_binding_service/openapi.yaml new file mode 100644 index 0000000..96b19e4 --- /dev/null +++ b/config_binding_service/openapi.yaml @@ -0,0 +1,112 @@ +openapi: 3.0.0 +info: + version: 2.4.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 must be a key in + consul. + required: true + schema: + 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 + content: + '*/*': + schema: + type: object + '404': + description: there is no configuration in Consul for this component + '/service_component_all/{service_component_name}': + parameters: + - name: service_component_name + in: path + description: >- + Service Component Name. service_component_name must be a key in + consul. + required: true + schema: + type: string + get: + description: >- + Binds the configuration for service_component_name and returns the bound + configuration, policies, and any other keys that are in Consul + operationId: config_binding_service.controller.bind_all + responses: + '200': + description: >- + OK; returns {config : ..., policies : ....., k : ...} for all other + k in Consul + content: + '*/*': + schema: + type: object + '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 + schema: + type: string + - name: service_component_name + in: path + description: Service Component Name. + required: true + schema: + 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' + content: + '*/*': + schema: + type: object + '400': + description: >- + bad request. Currently this is only returned on :policies, which is + a complex object, and should be gotten through service_component_all + content: + '*/*': + schema: + type: string + '404': + description: key does not exist + content: + '*/*': + 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 + operationId: config_binding_service.controller.healthcheck + responses: + '200': + description: Successful response + '503': + description: the config binding service cannot reach Consul diff --git a/config_binding_service/run.py b/config_binding_service/run.py new file mode 100755 index 0000000..175c0cf --- /dev/null +++ b/config_binding_service/run.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +# ============LICENSE_START======================================================= +# Copyright (c) 2017-2019 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 gevent.pywsgi import WSGIServer +from config_binding_service.logging import create_loggers, DEBUG_LOGGER +from config_binding_service import app + + +def main(): + """Entrypoint""" + if "PROD_LOGGING" in os.environ: + create_loggers() + DEBUG_LOGGER.debug("Starting gevent server") + http_server = WSGIServer(("", 10000), app) + http_server.serve_forever() |