1# -*- coding: utf-8 -*-
2# This code is part of Ansible, but is an independent component.
3# This particular file snippet, and this file snippet only, is BSD licensed.
4# Modules you write using this snippet, which is embedded dynamically by Ansible
5# still belong to the author of the module, and may assign their own license
6# to the complete work.
7#
8# Copyright (2016-2017) Hewlett Packard Enterprise Development LP
9#
10# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
11
12from __future__ import (absolute_import, division, print_function)
13__metaclass__ = type
14
15import abc
16import collections
17import json
18import os
19import traceback
20
21HPE_ONEVIEW_IMP_ERR = None
22try:
23    from hpOneView.oneview_client import OneViewClient
24    HAS_HPE_ONEVIEW = True
25except ImportError:
26    HPE_ONEVIEW_IMP_ERR = traceback.format_exc()
27    HAS_HPE_ONEVIEW = False
28
29from ansible.module_utils import six
30from ansible.module_utils.basic import AnsibleModule, missing_required_lib
31from ansible.module_utils.common.text.converters import to_native
32from ansible.module_utils.common._collections_compat import Mapping
33
34
35def transform_list_to_dict(list_):
36    """
37    Transforms a list into a dictionary, putting values as keys.
38
39    :arg list list_: List of values
40    :return: dict: dictionary built
41    """
42
43    ret = {}
44
45    if not list_:
46        return ret
47
48    for value in list_:
49        if isinstance(value, Mapping):
50            ret.update(value)
51        else:
52            ret[to_native(value, errors='surrogate_or_strict')] = True
53
54    return ret
55
56
57def merge_list_by_key(original_list, updated_list, key, ignore_when_null=None):
58    """
59    Merge two lists by the key. It basically:
60
61    1. Adds the items that are present on updated_list and are absent on original_list.
62
63    2. Removes items that are absent on updated_list and are present on original_list.
64
65    3. For all items that are in both lists, overwrites the values from the original item by the updated item.
66
67    :arg list original_list: original list.
68    :arg list updated_list: list with changes.
69    :arg str key: unique identifier.
70    :arg list ignore_when_null: list with the keys from the updated items that should be ignored in the merge,
71        if its values are null.
72    :return: list: Lists merged.
73    """
74    ignore_when_null = [] if ignore_when_null is None else ignore_when_null
75
76    if not original_list:
77        return updated_list
78
79    items_map = collections.OrderedDict([(i[key], i.copy()) for i in original_list])
80
81    merged_items = collections.OrderedDict()
82
83    for item in updated_list:
84        item_key = item[key]
85        if item_key in items_map:
86            for ignored_key in ignore_when_null:
87                if ignored_key in item and item[ignored_key] is None:
88                    item.pop(ignored_key)
89            merged_items[item_key] = items_map[item_key]
90            merged_items[item_key].update(item)
91        else:
92            merged_items[item_key] = item
93
94    return list(merged_items.values())
95
96
97def _str_sorted(obj):
98    if isinstance(obj, Mapping):
99        return json.dumps(obj, sort_keys=True)
100    else:
101        return str(obj)
102
103
104def _standardize_value(value):
105    """
106    Convert value to string to enhance the comparison.
107
108    :arg value: Any object type.
109
110    :return: str: Converted value.
111    """
112    if isinstance(value, float) and value.is_integer():
113        # Workaround to avoid erroneous comparison between int and float
114        # Removes zero from integer floats
115        value = int(value)
116
117    return str(value)
118
119
120class OneViewModuleException(Exception):
121    """
122    OneView base Exception.
123
124    Attributes:
125       msg (str): Exception message.
126       oneview_response (dict): OneView rest response.
127   """
128
129    def __init__(self, data):
130        self.msg = None
131        self.oneview_response = None
132
133        if isinstance(data, six.string_types):
134            self.msg = data
135        else:
136            self.oneview_response = data
137
138            if data and isinstance(data, dict):
139                self.msg = data.get('message')
140
141        if self.oneview_response:
142            Exception.__init__(self, self.msg, self.oneview_response)
143        else:
144            Exception.__init__(self, self.msg)
145
146
147class OneViewModuleTaskError(OneViewModuleException):
148    """
149    OneView Task Error Exception.
150
151    Attributes:
152       msg (str): Exception message.
153       error_code (str): A code which uniquely identifies the specific error.
154    """
155
156    def __init__(self, msg, error_code=None):
157        super(OneViewModuleTaskError, self).__init__(msg)
158        self.error_code = error_code
159
160
161class OneViewModuleValueError(OneViewModuleException):
162    """
163    OneView Value Error.
164    The exception is raised when the data contains an inappropriate value.
165
166    Attributes:
167       msg (str): Exception message.
168    """
169    pass
170
171
172class OneViewModuleResourceNotFound(OneViewModuleException):
173    """
174    OneView Resource Not Found Exception.
175    The exception is raised when an associated resource was not found.
176
177    Attributes:
178       msg (str): Exception message.
179    """
180    pass
181
182
183@six.add_metaclass(abc.ABCMeta)
184class OneViewModuleBase(object):
185    MSG_CREATED = 'Resource created successfully.'
186    MSG_UPDATED = 'Resource updated successfully.'
187    MSG_DELETED = 'Resource deleted successfully.'
188    MSG_ALREADY_PRESENT = 'Resource is already present.'
189    MSG_ALREADY_ABSENT = 'Resource is already absent.'
190    MSG_DIFF_AT_KEY = 'Difference found at key \'{0}\'. '
191
192    ONEVIEW_COMMON_ARGS = dict(
193        config=dict(type='path'),
194        hostname=dict(type='str'),
195        username=dict(type='str'),
196        password=dict(type='str', no_log=True),
197        api_version=dict(type='int'),
198        image_streamer_hostname=dict(type='str')
199    )
200
201    ONEVIEW_VALIDATE_ETAG_ARGS = dict(validate_etag=dict(type='bool', default=True))
202
203    resource_client = None
204
205    def __init__(self, additional_arg_spec=None, validate_etag_support=False, supports_check_mode=False):
206        """
207        OneViewModuleBase constructor.
208
209        :arg dict additional_arg_spec: Additional argument spec definition.
210        :arg bool validate_etag_support: Enables support to eTag validation.
211        """
212        argument_spec = self._build_argument_spec(additional_arg_spec, validate_etag_support)
213
214        self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=supports_check_mode)
215
216        self._check_hpe_oneview_sdk()
217        self._create_oneview_client()
218
219        self.state = self.module.params.get('state')
220        self.data = self.module.params.get('data')
221
222        # Preload params for get_all - used by facts
223        self.facts_params = self.module.params.get('params') or {}
224
225        # Preload options as dict - used by facts
226        self.options = transform_list_to_dict(self.module.params.get('options'))
227
228        self.validate_etag_support = validate_etag_support
229
230    def _build_argument_spec(self, additional_arg_spec, validate_etag_support):
231
232        merged_arg_spec = dict()
233        merged_arg_spec.update(self.ONEVIEW_COMMON_ARGS)
234
235        if validate_etag_support:
236            merged_arg_spec.update(self.ONEVIEW_VALIDATE_ETAG_ARGS)
237
238        if additional_arg_spec:
239            merged_arg_spec.update(additional_arg_spec)
240
241        return merged_arg_spec
242
243    def _check_hpe_oneview_sdk(self):
244        if not HAS_HPE_ONEVIEW:
245            self.module.fail_json(msg=missing_required_lib('hpOneView'), exception=HPE_ONEVIEW_IMP_ERR)
246
247    def _create_oneview_client(self):
248        if self.module.params.get('hostname'):
249            config = dict(ip=self.module.params['hostname'],
250                          credentials=dict(userName=self.module.params['username'], password=self.module.params['password']),
251                          api_version=self.module.params['api_version'],
252                          image_streamer_ip=self.module.params['image_streamer_hostname'])
253            self.oneview_client = OneViewClient(config)
254        elif not self.module.params['config']:
255            self.oneview_client = OneViewClient.from_environment_variables()
256        else:
257            self.oneview_client = OneViewClient.from_json_file(self.module.params['config'])
258
259    @abc.abstractmethod
260    def execute_module(self):
261        """
262        Abstract method, must be implemented by the inheritor.
263
264        This method is called from the run method. It should contains the module logic
265
266        :return: dict: It must return a dictionary with the attributes for the module result,
267            such as ansible_facts, msg and changed.
268        """
269        pass
270
271    def run(self):
272        """
273        Common implementation of the OneView run modules.
274
275        It calls the inheritor 'execute_module' function and sends the return to the Ansible.
276
277        It handles any OneViewModuleException in order to signal a failure to Ansible, with a descriptive error message.
278
279        """
280        try:
281            if self.validate_etag_support:
282                if not self.module.params.get('validate_etag'):
283                    self.oneview_client.connection.disable_etag_validation()
284
285            result = self.execute_module()
286
287            if "changed" not in result:
288                result['changed'] = False
289
290            self.module.exit_json(**result)
291
292        except OneViewModuleException as exception:
293            error_msg = '; '.join(to_native(e) for e in exception.args)
294            self.module.fail_json(msg=error_msg, exception=traceback.format_exc())
295
296    def resource_absent(self, resource, method='delete'):
297        """
298        Generic implementation of the absent state for the OneView resources.
299
300        It checks if the resource needs to be removed.
301
302        :arg dict resource: Resource to delete.
303        :arg str method: Function of the OneView client that will be called for resource deletion.
304            Usually delete or remove.
305        :return: A dictionary with the expected arguments for the AnsibleModule.exit_json
306        """
307        if resource:
308            getattr(self.resource_client, method)(resource)
309
310            return {"changed": True, "msg": self.MSG_DELETED}
311        else:
312            return {"changed": False, "msg": self.MSG_ALREADY_ABSENT}
313
314    def get_by_name(self, name):
315        """
316        Generic get by name implementation.
317
318        :arg str name: Resource name to search for.
319
320        :return: The resource found or None.
321        """
322        result = self.resource_client.get_by('name', name)
323        return result[0] if result else None
324
325    def resource_present(self, resource, fact_name, create_method='create'):
326        """
327        Generic implementation of the present state for the OneView resources.
328
329        It checks if the resource needs to be created or updated.
330
331        :arg dict resource: Resource to create or update.
332        :arg str fact_name: Name of the fact returned to the Ansible.
333        :arg str create_method: Function of the OneView client that will be called for resource creation.
334            Usually create or add.
335        :return: A dictionary with the expected arguments for the AnsibleModule.exit_json
336        """
337
338        changed = False
339        if "newName" in self.data:
340            self.data["name"] = self.data.pop("newName")
341
342        if not resource:
343            resource = getattr(self.resource_client, create_method)(self.data)
344            msg = self.MSG_CREATED
345            changed = True
346
347        else:
348            merged_data = resource.copy()
349            merged_data.update(self.data)
350
351            if self.compare(resource, merged_data):
352                msg = self.MSG_ALREADY_PRESENT
353            else:
354                resource = self.resource_client.update(merged_data)
355                changed = True
356                msg = self.MSG_UPDATED
357
358        return dict(
359            msg=msg,
360            changed=changed,
361            ansible_facts={fact_name: resource}
362        )
363
364    def resource_scopes_set(self, state, fact_name, scope_uris):
365        """
366        Generic implementation of the scopes update PATCH for the OneView resources.
367        It checks if the resource needs to be updated with the current scopes.
368        This method is meant to be run after ensuring the present state.
369        :arg dict state: Dict containing the data from the last state results in the resource.
370            It needs to have the 'msg', 'changed', and 'ansible_facts' entries.
371        :arg str fact_name: Name of the fact returned to the Ansible.
372        :arg list scope_uris: List with all the scope URIs to be added to the resource.
373        :return: A dictionary with the expected arguments for the AnsibleModule.exit_json
374        """
375        if scope_uris is None:
376            scope_uris = []
377        resource = state['ansible_facts'][fact_name]
378        operation_data = dict(operation='replace', path='/scopeUris', value=scope_uris)
379
380        if resource['scopeUris'] is None or set(resource['scopeUris']) != set(scope_uris):
381            state['ansible_facts'][fact_name] = self.resource_client.patch(resource['uri'], **operation_data)
382            state['changed'] = True
383            state['msg'] = self.MSG_UPDATED
384
385        return state
386
387    def compare(self, first_resource, second_resource):
388        """
389        Recursively compares dictionary contents equivalence, ignoring types and elements order.
390        Particularities of the comparison:
391            - Inexistent key = None
392            - These values are considered equal: None, empty, False
393            - Lists are compared value by value after a sort, if they have same size.
394            - Each element is converted to str before the comparison.
395        :arg dict first_resource: first dictionary
396        :arg dict second_resource: second dictionary
397        :return: bool: True when equal, False when different.
398        """
399        resource1 = first_resource
400        resource2 = second_resource
401
402        debug_resources = "resource1 = {0}, resource2 = {1}".format(resource1, resource2)
403
404        # The first resource is True / Not Null and the second resource is False / Null
405        if resource1 and not resource2:
406            self.module.log("resource1 and not resource2. " + debug_resources)
407            return False
408
409        # Checks all keys in first dict against the second dict
410        for key in resource1:
411            if key not in resource2:
412                if resource1[key] is not None:
413                    # Inexistent key is equivalent to exist with value None
414                    self.module.log(self.MSG_DIFF_AT_KEY.format(key) + debug_resources)
415                    return False
416            # If both values are null, empty or False it will be considered equal.
417            elif not resource1[key] and not resource2[key]:
418                continue
419            elif isinstance(resource1[key], Mapping):
420                # recursive call
421                if not self.compare(resource1[key], resource2[key]):
422                    self.module.log(self.MSG_DIFF_AT_KEY.format(key) + debug_resources)
423                    return False
424            elif isinstance(resource1[key], list):
425                # change comparison function to compare_list
426                if not self.compare_list(resource1[key], resource2[key]):
427                    self.module.log(self.MSG_DIFF_AT_KEY.format(key) + debug_resources)
428                    return False
429            elif _standardize_value(resource1[key]) != _standardize_value(resource2[key]):
430                self.module.log(self.MSG_DIFF_AT_KEY.format(key) + debug_resources)
431                return False
432
433        # Checks all keys in the second dict, looking for missing elements
434        for key in resource2.keys():
435            if key not in resource1:
436                if resource2[key] is not None:
437                    # Inexistent key is equivalent to exist with value None
438                    self.module.log(self.MSG_DIFF_AT_KEY.format(key) + debug_resources)
439                    return False
440
441        return True
442
443    def compare_list(self, first_resource, second_resource):
444        """
445        Recursively compares lists contents equivalence, ignoring types and element orders.
446        Lists with same size are compared value by value after a sort,
447        each element is converted to str before the comparison.
448        :arg list first_resource: first list
449        :arg list second_resource: second list
450        :return: True when equal; False when different.
451        """
452
453        resource1 = first_resource
454        resource2 = second_resource
455
456        debug_resources = "resource1 = {0}, resource2 = {1}".format(resource1, resource2)
457
458        # The second list is null / empty  / False
459        if not resource2:
460            self.module.log("resource 2 is null. " + debug_resources)
461            return False
462
463        if len(resource1) != len(resource2):
464            self.module.log("resources have different length. " + debug_resources)
465            return False
466
467        resource1 = sorted(resource1, key=_str_sorted)
468        resource2 = sorted(resource2, key=_str_sorted)
469
470        for i, val in enumerate(resource1):
471            if isinstance(val, Mapping):
472                # change comparison function to compare dictionaries
473                if not self.compare(val, resource2[i]):
474                    self.module.log("resources are different. " + debug_resources)
475                    return False
476            elif isinstance(val, list):
477                # recursive call
478                if not self.compare_list(val, resource2[i]):
479                    self.module.log("lists are different. " + debug_resources)
480                    return False
481            elif _standardize_value(val) != _standardize_value(resource2[i]):
482                self.module.log("values are different. " + debug_resources)
483                return False
484
485        # no differences found
486        return True
487