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