1# -*- coding: utf-8 -*-
2# Copyright (c), Google Inc, 2017
3# Simplified BSD License (see licenses/simplified_bsd.txt or
4# https://opensource.org/licenses/BSD-2-Clause)
5
6from __future__ import (absolute_import, division, print_function)
7__metaclass__ = type
8
9import re
10import time
11import traceback
12
13THIRD_LIBRARIES_IMP_ERR = None
14try:
15    from keystoneauth1.adapter import Adapter
16    from keystoneauth1.identity import v3
17    from keystoneauth1 import session
18    HAS_THIRD_LIBRARIES = True
19except ImportError:
20    THIRD_LIBRARIES_IMP_ERR = traceback.format_exc()
21    HAS_THIRD_LIBRARIES = False
22
23from ansible.module_utils.basic import (AnsibleModule, env_fallback,
24                                        missing_required_lib)
25from ansible.module_utils.common.text.converters import to_text
26
27
28class HwcModuleException(Exception):
29    def __init__(self, message):
30        super(HwcModuleException, self).__init__()
31
32        self._message = message
33
34    def __str__(self):
35        return "[HwcClientException] message=%s" % self._message
36
37
38class HwcClientException(Exception):
39    def __init__(self, code, message):
40        super(HwcClientException, self).__init__()
41
42        self._code = code
43        self._message = message
44
45    def __str__(self):
46        msg = " code=%s," % str(self._code) if self._code != 0 else ""
47        return "[HwcClientException]%s message=%s" % (
48            msg, self._message)
49
50
51class HwcClientException404(HwcClientException):
52    def __init__(self, message):
53        super(HwcClientException404, self).__init__(404, message)
54
55    def __str__(self):
56        return "[HwcClientException404] message=%s" % self._message
57
58
59def session_method_wrapper(f):
60    def _wrap(self, url, *args, **kwargs):
61        try:
62            url = self.endpoint + url
63            r = f(self, url, *args, **kwargs)
64        except Exception as ex:
65            raise HwcClientException(
66                0, "Sending request failed, error=%s" % ex)
67
68        result = None
69        if r.content:
70            try:
71                result = r.json()
72            except Exception as ex:
73                raise HwcClientException(
74                    0, "Parsing response to json failed, error: %s" % ex)
75
76        code = r.status_code
77        if code not in [200, 201, 202, 203, 204, 205, 206, 207, 208, 226]:
78            msg = ""
79            for i in ['message', 'error.message']:
80                try:
81                    msg = navigate_value(result, i)
82                    break
83                except Exception:
84                    pass
85            else:
86                msg = str(result)
87
88            if code == 404:
89                raise HwcClientException404(msg)
90
91            raise HwcClientException(code, msg)
92
93        return result
94
95    return _wrap
96
97
98class _ServiceClient(object):
99    def __init__(self, client, endpoint, product):
100        self._client = client
101        self._endpoint = endpoint
102        self._default_header = {
103            'User-Agent': "Huawei-Ansible-MM-%s" % product,
104            'Accept': 'application/json',
105        }
106
107    @property
108    def endpoint(self):
109        return self._endpoint
110
111    @endpoint.setter
112    def endpoint(self, e):
113        self._endpoint = e
114
115    @session_method_wrapper
116    def get(self, url, body=None, header=None, timeout=None):
117        return self._client.get(url, json=body, timeout=timeout,
118                                headers=self._header(header))
119
120    @session_method_wrapper
121    def post(self, url, body=None, header=None, timeout=None):
122        return self._client.post(url, json=body, timeout=timeout,
123                                 headers=self._header(header))
124
125    @session_method_wrapper
126    def delete(self, url, body=None, header=None, timeout=None):
127        return self._client.delete(url, json=body, timeout=timeout,
128                                   headers=self._header(header))
129
130    @session_method_wrapper
131    def put(self, url, body=None, header=None, timeout=None):
132        return self._client.put(url, json=body, timeout=timeout,
133                                headers=self._header(header))
134
135    def _header(self, header):
136        if header and isinstance(header, dict):
137            for k, v in self._default_header.items():
138                if k not in header:
139                    header[k] = v
140        else:
141            header = self._default_header
142
143        return header
144
145
146class Config(object):
147    def __init__(self, module, product):
148        self._project_client = None
149        self._domain_client = None
150        self._module = module
151        self._product = product
152        self._endpoints = {}
153
154        self._validate()
155        self._gen_provider_client()
156
157    @property
158    def module(self):
159        return self._module
160
161    def client(self, region, service_type, service_level):
162        c = self._project_client
163        if service_level == "domain":
164            c = self._domain_client
165
166        e = self._get_service_endpoint(c, service_type, region)
167
168        return _ServiceClient(c, e, self._product)
169
170    def _gen_provider_client(self):
171        m = self._module
172        p = {
173            "auth_url": m.params['identity_endpoint'],
174            "password": m.params['password'],
175            "username": m.params['user'],
176            "project_name": m.params['project'],
177            "user_domain_name": m.params['domain'],
178            "reauthenticate": True
179        }
180
181        self._project_client = Adapter(
182            session.Session(auth=v3.Password(**p)),
183            raise_exc=False)
184
185        p.pop("project_name")
186        self._domain_client = Adapter(
187            session.Session(auth=v3.Password(**p)),
188            raise_exc=False)
189
190    def _get_service_endpoint(self, client, service_type, region):
191        k = "%s.%s" % (service_type, region if region else "")
192
193        if k in self._endpoints:
194            return self._endpoints.get(k)
195
196        url = None
197        try:
198            url = client.get_endpoint(service_type=service_type,
199                                      region_name=region, interface="public")
200        except Exception as ex:
201            raise HwcClientException(
202                0, "Getting endpoint failed, error=%s" % ex)
203
204        if url == "":
205            raise HwcClientException(
206                0, "Can not find the enpoint for %s" % service_type)
207
208        if url[-1] != "/":
209            url += "/"
210
211        self._endpoints[k] = url
212        return url
213
214    def _validate(self):
215        if not HAS_THIRD_LIBRARIES:
216            self.module.fail_json(
217                msg=missing_required_lib('keystoneauth1'),
218                exception=THIRD_LIBRARIES_IMP_ERR)
219
220
221class HwcModule(AnsibleModule):
222    def __init__(self, *args, **kwargs):
223        arg_spec = kwargs.setdefault('argument_spec', {})
224
225        arg_spec.update(
226            dict(
227                identity_endpoint=dict(
228                    required=True, type='str',
229                    fallback=(env_fallback, ['ANSIBLE_HWC_IDENTITY_ENDPOINT']),
230                ),
231                user=dict(
232                    required=True, type='str',
233                    fallback=(env_fallback, ['ANSIBLE_HWC_USER']),
234                ),
235                password=dict(
236                    required=True, type='str', no_log=True,
237                    fallback=(env_fallback, ['ANSIBLE_HWC_PASSWORD']),
238                ),
239                domain=dict(
240                    required=True, type='str',
241                    fallback=(env_fallback, ['ANSIBLE_HWC_DOMAIN']),
242                ),
243                project=dict(
244                    required=True, type='str',
245                    fallback=(env_fallback, ['ANSIBLE_HWC_PROJECT']),
246                ),
247                region=dict(
248                    type='str',
249                    fallback=(env_fallback, ['ANSIBLE_HWC_REGION']),
250                ),
251                id=dict(type='str')
252            )
253        )
254
255        super(HwcModule, self).__init__(*args, **kwargs)
256
257
258class _DictComparison(object):
259    ''' This class takes in two dictionaries `a` and `b`.
260        These are dictionaries of arbitrary depth, but made up of standard
261        Python types only.
262        This differ will compare all values in `a` to those in `b`.
263        If value in `a` is None, always returns True, indicating
264        this value is no need to compare.
265        Note: On all lists, order does matter.
266    '''
267
268    def __init__(self, request):
269        self.request = request
270
271    def __eq__(self, other):
272        return self._compare_dicts(self.request, other.request)
273
274    def __ne__(self, other):
275        return not self.__eq__(other)
276
277    def _compare_dicts(self, dict1, dict2):
278        if dict1 is None:
279            return True
280
281        if set(dict1.keys()) != set(dict2.keys()):
282            return False
283
284        for k in dict1:
285            if not self._compare_value(dict1.get(k), dict2.get(k)):
286                return False
287
288        return True
289
290    def _compare_lists(self, list1, list2):
291        """Takes in two lists and compares them."""
292        if list1 is None:
293            return True
294
295        if len(list1) != len(list2):
296            return False
297
298        for i in range(len(list1)):
299            if not self._compare_value(list1[i], list2[i]):
300                return False
301
302        return True
303
304    def _compare_value(self, value1, value2):
305        """
306        return: True: value1 is same as value2, otherwise False.
307        """
308        if value1 is None:
309            return True
310
311        if not (value1 and value2):
312            return (not value1) and (not value2)
313
314        # Can assume non-None types at this point.
315        if isinstance(value1, list) and isinstance(value2, list):
316            return self._compare_lists(value1, value2)
317
318        elif isinstance(value1, dict) and isinstance(value2, dict):
319            return self._compare_dicts(value1, value2)
320
321        # Always use to_text values to avoid unicode issues.
322        return (to_text(value1, errors='surrogate_or_strict') == to_text(
323            value2, errors='surrogate_or_strict'))
324
325
326def wait_to_finish(target, pending, refresh, timeout, min_interval=1, delay=3):
327    is_last_time = False
328    not_found_times = 0
329    wait = 0
330
331    time.sleep(delay)
332
333    end = time.time() + timeout
334    while not is_last_time:
335        if time.time() > end:
336            is_last_time = True
337
338        obj, status = refresh()
339
340        if obj is None:
341            not_found_times += 1
342
343            if not_found_times > 10:
344                raise HwcModuleException(
345                    "not found the object for %d times" % not_found_times)
346        else:
347            not_found_times = 0
348
349            if status in target:
350                return obj
351
352            if pending and status not in pending:
353                raise HwcModuleException(
354                    "unexpect status(%s) occured" % status)
355
356        if not is_last_time:
357            wait *= 2
358            if wait < min_interval:
359                wait = min_interval
360            elif wait > 10:
361                wait = 10
362
363            time.sleep(wait)
364
365    raise HwcModuleException("asycn wait timeout after %d seconds" % timeout)
366
367
368def navigate_value(data, index, array_index=None):
369    if array_index and (not isinstance(array_index, dict)):
370        raise HwcModuleException("array_index must be dict")
371
372    d = data
373    for n in range(len(index)):
374        if d is None:
375            return None
376
377        if not isinstance(d, dict):
378            raise HwcModuleException(
379                "can't navigate value from a non-dict object")
380
381        i = index[n]
382        if i not in d:
383            raise HwcModuleException(
384                "navigate value failed: key(%s) is not exist in dict" % i)
385        d = d[i]
386
387        if not array_index:
388            continue
389
390        k = ".".join(index[: (n + 1)])
391        if k not in array_index:
392            continue
393
394        if d is None:
395            return None
396
397        if not isinstance(d, list):
398            raise HwcModuleException(
399                "can't navigate value from a non-list object")
400
401        j = array_index.get(k)
402        if j >= len(d):
403            raise HwcModuleException(
404                "navigate value failed: the index is out of list")
405        d = d[j]
406
407    return d
408
409
410def build_path(module, path, kv=None):
411    if kv is None:
412        kv = dict()
413
414    v = {}
415    for p in re.findall(r"{[^/]*}", path):
416        n = p[1:][:-1]
417
418        if n in kv:
419            v[n] = str(kv[n])
420
421        else:
422            if n in module.params:
423                v[n] = str(module.params.get(n))
424            else:
425                v[n] = ""
426
427    return path.format(**v)
428
429
430def get_region(module):
431    if module.params['region']:
432        return module.params['region']
433
434    return module.params['project'].split("_")[0]
435
436
437def is_empty_value(v):
438    return (not v)
439
440
441def are_different_dicts(dict1, dict2):
442    return _DictComparison(dict1) != _DictComparison(dict2)
443