1# Copyright 2011, VMware, Inc.
2#
3#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4#    not use this file except in compliance with the License. You may obtain
5#    a copy of the License at
6#
7#         http://www.apache.org/licenses/LICENSE-2.0
8#
9#    Unless required by applicable law or agreed to in writing, software
10#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12#    License for the specific language governing permissions and limitations
13#    under the License.
14#
15# Borrowed from nova code base, more utilities will be added/borrowed as and
16# when needed.
17
18"""Utilities and helper functions."""
19
20import argparse
21import functools
22import hashlib
23import logging
24import os
25
26from oslo_utils import encodeutils
27from oslo_utils import importutils
28
29from neutronclient._i18n import _
30from neutronclient.common import exceptions
31
32SENSITIVE_HEADERS = ('X-Auth-Token',)
33
34
35def env(*vars, **kwargs):
36    """Returns the first environment variable set.
37
38    If none are non-empty, defaults to '' or keyword arg default.
39    """
40    for v in vars:
41        value = os.environ.get(v)
42        if value:
43            return value
44    return kwargs.get('default', '')
45
46
47def convert_to_uppercase(string):
48    return string.upper()
49
50
51def convert_to_lowercase(string):
52    return string.lower()
53
54
55def get_client_class(api_name, version, version_map):
56    """Returns the client class for the requested API version.
57
58    :param api_name: the name of the API, e.g. 'compute', 'image', etc
59    :param version: the requested API version
60    :param version_map: a dict of client classes keyed by version
61    :rtype: a client class for the requested API version
62    """
63    try:
64        client_path = version_map[str(version)]
65    except (KeyError, ValueError):
66        msg = _("Invalid %(api_name)s client version '%(version)s'. must be "
67                "one of: %(map_keys)s")
68        msg = msg % {'api_name': api_name, 'version': version,
69                     'map_keys': ', '.join(version_map.keys())}
70        raise exceptions.UnsupportedVersion(msg)
71
72    return importutils.import_class(client_path)
73
74
75def get_item_properties(item, fields, mixed_case_fields=(), formatters=None):
76    """Return a tuple containing the item properties.
77
78    :param item: a single item resource (e.g. Server, Tenant, etc)
79    :param fields: tuple of strings with the desired field names
80    :param mixed_case_fields: tuple of field names to preserve case
81    :param formatters: dictionary mapping field names to callables
82       to format the values
83    """
84    if formatters is None:
85        formatters = {}
86
87    row = []
88
89    for field in fields:
90        if field in formatters:
91            row.append(formatters[field](item))
92        else:
93            if field in mixed_case_fields:
94                field_name = field.replace(' ', '_')
95            else:
96                field_name = field.lower().replace(' ', '_')
97            if not hasattr(item, field_name) and isinstance(item, dict):
98                data = item[field_name]
99            else:
100                data = getattr(item, field_name, '')
101            if data is None:
102                data = ''
103            row.append(data)
104    return tuple(row)
105
106
107def str2bool(strbool):
108    if strbool is None:
109        return None
110    return strbool.lower() == 'true'
111
112
113def str2dict(strdict, required_keys=None, optional_keys=None):
114    """Convert key1=value1,key2=value2,... string into dictionary.
115
116    :param strdict: string in the form of key1=value1,key2=value2
117    :param required_keys: list of required keys. All keys in this list must be
118                       specified. Otherwise ArgumentTypeError will be raised.
119                       If this parameter is unspecified, no required key check
120                       will be done.
121    :param optional_keys: list of optional keys.
122                       This parameter is used for valid key check.
123                       When at least one of required_keys and optional_keys,
124                       a key must be a member of either of required_keys or
125                       optional_keys. Otherwise, ArgumentTypeError will be
126                       raised. When both required_keys and optional_keys are
127                       unspecified, no valid key check will be done.
128    """
129    result = {}
130    if strdict:
131        i = 0
132        kvlist = []
133        for kv in strdict.split(','):
134            if '=' in kv:
135                kvlist.append(kv)
136                i += 1
137            elif i == 0:
138                msg = _("missing value for key '%s'")
139                raise argparse.ArgumentTypeError(msg % kv)
140            else:
141                kvlist[i-1] = "%s,%s" % (kvlist[i-1], kv)
142        for kv in kvlist:
143            key, sep, value = kv.partition('=')
144            if not sep:
145                msg = _("invalid key-value '%s', expected format: key=value")
146                raise argparse.ArgumentTypeError(msg % kv)
147            result[key] = value
148    valid_keys = set(required_keys or []) | set(optional_keys or [])
149    if valid_keys:
150        invalid_keys = [k for k in result if k not in valid_keys]
151        if invalid_keys:
152            msg = _("Invalid key(s) '%(invalid_keys)s' specified. "
153                    "Valid key(s): '%(valid_keys)s'.")
154            raise argparse.ArgumentTypeError(
155                msg % {'invalid_keys': ', '.join(sorted(invalid_keys)),
156                       'valid_keys': ', '.join(sorted(valid_keys))})
157    if required_keys:
158        not_found_keys = [k for k in required_keys if k not in result]
159        if not_found_keys:
160            msg = _("Required key(s) '%s' not specified.")
161            raise argparse.ArgumentTypeError(msg % ', '.join(not_found_keys))
162    return result
163
164
165def str2dict_type(optional_keys=None, required_keys=None):
166    return functools.partial(str2dict,
167                             optional_keys=optional_keys,
168                             required_keys=required_keys)
169
170
171def http_log_req(_logger, args, kwargs):
172    if not _logger.isEnabledFor(logging.DEBUG):
173        return
174
175    string_parts = ['curl -i']
176    for element in args:
177        if element in ('GET', 'POST', 'DELETE', 'PUT'):
178            string_parts.append(' -X %s' % element)
179        else:
180            string_parts.append(' %s' % element)
181
182    for (key, value) in kwargs['headers'].items():
183        if key in SENSITIVE_HEADERS:
184            v = value.encode('utf-8')
185            h = hashlib.sha256(v)
186            d = h.hexdigest()
187            value = "{SHA256}%s" % d
188        header = ' -H "%s: %s"' % (key, value)
189        string_parts.append(header)
190
191    if 'body' in kwargs and kwargs['body']:
192        string_parts.append(" -d '%s'" % (kwargs['body']))
193    req = encodeutils.safe_encode("".join(string_parts))
194    _logger.debug("REQ: %s", req)
195
196
197def http_log_resp(_logger, resp, body):
198    if not _logger.isEnabledFor(logging.DEBUG):
199        return
200    _logger.debug("RESP: %(code)s %(headers)s %(body)s",
201                  {'code': resp.status_code,
202                   'headers': resp.headers,
203                   'body': body})
204
205
206def _safe_encode_without_obj(data):
207    if isinstance(data, str):
208        return encodeutils.safe_encode(data)
209    return data
210
211
212def safe_encode_list(data):
213    return list(map(_safe_encode_without_obj, data))
214
215
216def safe_encode_dict(data):
217    def _encode_item(item):
218        k, v = item
219        if isinstance(v, list):
220            return (k, safe_encode_list(v))
221        elif isinstance(v, dict):
222            return (k, safe_encode_dict(v))
223        return (k, _safe_encode_without_obj(v))
224
225    return dict(list(map(_encode_item, data.items())))
226
227
228def add_boolean_argument(parser, name, **kwargs):
229    for keyword in ('metavar', 'choices'):
230        kwargs.pop(keyword, None)
231    default = kwargs.pop('default', argparse.SUPPRESS)
232    parser.add_argument(
233        name,
234        metavar='{True,False}',
235        choices=['True', 'true', 'False', 'false'],
236        default=default,
237        **kwargs)
238