summaryrefslogtreecommitdiffstats
path: root/onap-dcae-cbs-docker-client/onap_dcae_cbs_docker_client/client.py
blob: a9bfa34c5b07e973a02e2041eb3b38281de4f54b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# ================================================================================
# Copyright (c) 2017-2021 AT&T Intellectual Property. All rights reserved.
# Copyright (C) 2021 Nokia. 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=========================================================

""" provide a means to return a configuration to CBS """

import json
import os
import requests
import sys
import yaml

from onap_dcae_cbs_docker_client import get_module_logger
from onap_dcae_cbs_docker_client.exceptions import ENVsMissing, CantGetConfig, CBSUnreachable

logger = get_module_logger(__name__)


DEFAULT_CONFIG_PATH = "/app-config/application_config.yaml"
DEFAULT_POLICY_PATH = "/etc/policies/policies.json"


# provide a means to import the default paths into unit tests
# For some reason, tox does not like
# from onap_dcae_cbs_docker_client.client import DEFAULT_CONFIG_PATH, DEFAULT_POLICY_PATH

def default_config_path():
    return DEFAULT_CONFIG_PATH


def default_policy_path():
    return DEFAULT_POLICY_PATH

#########
# HELPERS


def _recurse(config):
    """
    Recurse through a configuration, or recursively a sub element 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: expand the string and return its replacement
    If none of the above, just return the item.
    """
    if isinstance(config, list):
        return [_recurse(item) for item in config]
    if isinstance(config, dict):
        for key in config:
            config[key] = _recurse(config[key])
        return config
    if isinstance(config, str):
        return change_envs(config)
    # not a dict, not a list, not a string, nothing to do.
    return config


def _get_path(path):
    """
    Try to get the config, and return appropriate exceptions otherwise
    """
    try:
        hostname = os.environ["HOSTNAME"]  # this is the name of the component itself
        # in most cases, this is the K8s service name which is a resolvable DNS name
        # if running outside k8s, this name needs to be resolvable by DNS via other means.
        cbs_name = os.environ["CONFIG_BINDING_SERVICE"]
    except KeyError as e:
        raise ENVsMissing("Required ENV Variable {0} missing".format(e))

    # See if we are using https
    https_cacert = os.environ.get("DCAE_CA_CERTPATH", None)

    # Get the CBS URL.
    cbs_url = "https://{0}:10443".format(cbs_name) if https_cacert else "http://{0}:10000".format(cbs_name)

    # get my config
    try:
        my_config_endpoint = "{0}/{1}/{2}".format(cbs_url, path, hostname)
        res = requests.get(my_config_endpoint, verify=https_cacert) if https_cacert else requests.get(my_config_endpoint)
        res.raise_for_status()
        config = res.json()
        logger.debug(
            "get_config returned the following configuration: %s using the config url %s",
            json.dumps(config),
            my_config_endpoint,
        )
        return config
    except requests.exceptions.HTTPError:  # this is thrown by raise_for_status
        logger.error(
            "The config binding service endpoint %s returned a bad status. code: %d, text: %s",
            my_config_endpoint,
            res.status_code,
            res.text,
        )
        raise CantGetConfig(res.status_code, res.text)
    except requests.exceptions.ConnectionError as e:  # this is thrown if requests.get cant even connect to the endpoint
        raise CBSUnreachable(e)


def change_envs(value):
    """
    Replace env reference by actual value and return it
    """
    if value.startswith('${'):
        name = value[2:-1]
        if name in os.environ:
            return os.environ[name]

        logger.error(f"Required ENV Variable '{name}' missing. Is '{value}' properly formatted?")
        raise ENVsMissing(f"Required ENV Variable {name} missing. Is '{value}' properly formatted?")
    return value


#########
# Public
def get_all():
    """
    If not configured locally,
    hit the CBS service_component_all endpoint

    Local configuration comes from $CBS_CLIENT_CONFIG_PATH and $CBS_CLIENT_POLICY_PATH,
    defaulted to /app-config/application_config.yaml and /etc/policies/policies.json.
    """
    config_path = os.getenv("CBS_CLIENT_CONFIG_PATH", DEFAULT_CONFIG_PATH)
    if config_path == "":
        config_path = DEFAULT_CONFIG_PATH
    policy_path = os.getenv("CBS_CLIENT_POLICY_PATH", DEFAULT_POLICY_PATH)
    if policy_path == "":
        policy_path = DEFAULT_POLICY_PATH

    try:
        logger.debug(f"opening config_path={config_path}")
        with open(config_path) as fp:
            config = yaml.safe_load(fp)

        policies = None
        try:
            logger.debug(f"opening policy_path={policy_path}")
            with open(policy_path) as fp:
                policies = json.load(fp)

        except FileNotFoundError:
            logger.debug("Policy File Not Found exception received")
            pass

        except (json.decoder.JSONDecodeError, ValueError) as e:
            logger.error(f"The policy file '{policy_path}' has invalid JSON: %s", e)
            pass

        except Exception as e:
            logger.error(f"An error occurred processing the policy file '{policy_path}': %s", e)
            import traceback
            traceback.print_exc(file=sys.stderr)
            pass

        if policies is not None:
            if "policies" in policies:
                logger.debug(f"Returning config read from {config_path} an policy read from {policy_path}")
                ret = {"config": _recurse(config), "policies": policies["policies"]}
            else:
                logger.error(f"The policy file '{policy_path}' does NOT have a 'policies' block in it.")
                ret = {"config": _recurse(config)}
        else:
            logger.debug(f"Returning config read from {config_path}")
            ret = {"config": _recurse(config)}

        return ret

    except yaml.scanner.ScannerError as e:
        logger.error(f"The configuration file '{config_path}' has invalid YAML: {e}")
        pass

    except FileNotFoundError:
        logger.debug("Config File Not Found exception received")
        pass

    except Exception as e:
        logger.error(f"An error occurred processing the configuration file '{config_path}': {e}")
        import traceback
        traceback.print_exc(file=sys.stderr)
        pass

    logger.debug("Fallback to using REST call")
    config = _get_path("service_component_all")
    return _recurse(config)


def get_config():
    """
    If not configured locally,
    hit the CBS service_component endpoint for the configuration.

    Local configuration comes from $CBS_CLIENT_CONFIG_PATH,
    defaulted to /app-config/application_config.yaml.

    TODO: should we take in a "retry" boolean, and retry on behalf of the caller?
    Currently, we return an exception and let the application decide how it wants to proceed (Crash, try again, etc).
    """
    config_path = os.getenv("CBS_CLIENT_CONFIG_PATH", DEFAULT_CONFIG_PATH)
    if config_path == "":
        config_path = DEFAULT_CONFIG_PATH

    try:
        logger.debug(f"opening config_path={config_path}")
        with open(config_path) as fp:
            config = yaml.safe_load(fp)

        logger.debug(f"Returning config read from {config_path}")
        return _recurse(config)

    except yaml.scanner.ScannerError as e:
        logger.error(f"The configuration file '{config_path}' has invalid YAML: {e}")
        pass

    except FileNotFoundError:
        logger.debug("Config File Not Found exception received")
        pass

    except Exception as e:
        logger.error(f"An error occurred processing the configuration file '{config_path}': {e}")
        import traceback
        traceback.print_exc(file=sys.stderr)
        pass

    logger.debug("Fallback to using REST call")
    config = _get_path("service_component")
    return _recurse(config)