From 5fd572d186c4caf467a240eeb9c75a5a3087a295 Mon Sep 17 00:00:00 2001 From: fgbulsoni Date: Thu, 22 Jun 2017 16:24:53 -0300 Subject: [PATCH 01/19] Initial commit for integration of HPE OneView resources with Ansible Core. Adding FC Network and FC Network Fact modules and unit tests, and OneView base class for all OV resources. --- lib/ansible/module_utils/oneview.py | 771 ++++++++++++++++++ lib/ansible/modules/cloud/hpe/__init__.py | 0 .../modules/cloud/hpe/oneview_fc_network.py | 121 +++ .../cloud/hpe/oneview_fc_network_facts.py | 108 +++ .../utils/module_docs_fragments/oneview.py | 62 ++ test/runner/requirements/units.txt | 1 + .../units/modules/cloud/hpe/hpe_test_utils.py | 136 +++ .../cloud/hpe/oneview_module_loader.py | 35 + .../cloud/hpe/test_oneview_fc_network.py | 134 +++ .../hpe/test_oneview_fc_network_facts.py | 73 ++ 10 files changed, 1441 insertions(+) create mode 100644 lib/ansible/module_utils/oneview.py create mode 100644 lib/ansible/modules/cloud/hpe/__init__.py create mode 100644 lib/ansible/modules/cloud/hpe/oneview_fc_network.py create mode 100644 lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py create mode 100644 lib/ansible/utils/module_docs_fragments/oneview.py create mode 100644 test/units/modules/cloud/hpe/hpe_test_utils.py create mode 100644 test/units/modules/cloud/hpe/oneview_module_loader.py create mode 100644 test/units/modules/cloud/hpe/test_oneview_fc_network.py create mode 100644 test/units/modules/cloud/hpe/test_oneview_fc_network_facts.py diff --git a/lib/ansible/module_utils/oneview.py b/lib/ansible/module_utils/oneview.py new file mode 100644 index 00000000000000..89b8d55330d2cb --- /dev/null +++ b/lib/ansible/module_utils/oneview.py @@ -0,0 +1,771 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (2016-2017) Hewlett Packard Enterprise Development LP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import (absolute_import, + division, + print_function, + unicode_literals) + +from future import standard_library +import json +import logging +import os +from ansible.module_utils.basic import AnsibleModule +from copy import deepcopy +from collections import OrderedDict + +standard_library.install_aliases() +logger = logging.getLogger(__name__) + +try: + from hpOneView.oneview_client import OneViewClient + from hpOneView.exceptions import (HPOneViewException, + HPOneViewTaskError, + HPOneViewValueError, + HPOneViewResourceNotFound) + + HAS_HPE_ONEVIEW = True +except ImportError: + HAS_HPE_ONEVIEW = False + + +class OneViewModuleBase(object): + MSG_CREATED = 'Resource created successfully.' + MSG_UPDATED = 'Resource updated successfully.' + MSG_DELETED = 'Resource deleted successfully.' + MSG_ALREADY_PRESENT = 'Resource is already present.' + MSG_ALREADY_ABSENT = 'Resource is already absent.' + HPE_ONEVIEW_SDK_REQUIRED = 'HPE OneView Python SDK is required for this module.' + + ONEVIEW_COMMON_ARGS = dict( + config=dict(required=False, type='str') + ) + + ONEVIEW_VALIDATE_ETAG_ARGS = dict( + validate_etag=dict( + required=False, + type='bool', + default=True) + ) + + resource_client = None + + def __init__(self, additional_arg_spec=None, validate_etag_support=False): + """ + OneViewModuleBase constructor. + + Args: + additional_arg_spec (dict): Additional argument spec definition. + validate_etag_support (bool): Enables support to eTag validation. + """ + + argument_spec = self.__build_argument_spec(additional_arg_spec, validate_etag_support) + + self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + + self.__check_hpe_oneview_sdk() + self.__create_oneview_client() + + self.state = self.module.params.get('state') + self.data = self.module.params.get('data') + + # Preload params for get_all - used by facts + self.facts_params = self.module.params.get('params') or {} + + # Preload options as dict - used by facts + self.options = self.transform_list_to_dict(self.module.params.get('options')) + + self.validate_etag_support = validate_etag_support + + def __build_argument_spec(self, additional_arg_spec, validate_etag_support): + + merged_arg_spec = dict() + merged_arg_spec.update(self.ONEVIEW_COMMON_ARGS) + + if validate_etag_support: + merged_arg_spec.update(self.ONEVIEW_VALIDATE_ETAG_ARGS) + + if additional_arg_spec: + merged_arg_spec.update(additional_arg_spec) + + return merged_arg_spec + + def __check_hpe_oneview_sdk(self): + if not HAS_HPE_ONEVIEW: + self.module.fail_json(msg=self.HPE_ONEVIEW_SDK_REQUIRED) + + def __create_oneview_client(self): + if not self.module.params['config']: + self.oneview_client = OneViewClient.from_environment_variables() + else: + self.oneview_client = OneViewClient.from_json_file(self.module.params['config']) + + def execute_module(self): + """ + Abstract function, must be implemented by the inheritor. + + This method is called from the run method. It should contains the module logic + + Returns: + dict: + It must return a dictionary with the attributes for the module result, + such as ansible_facts, msg and changed. + """ + raise HPOneViewException("execute_module not implemented") + + def run(self): + """ + Common implementation of the OneView run modules. + It calls the inheritor 'execute_module' function and sends the return to the Ansible. + It handles any HPOneViewException in order to signal a failure to Ansible, with a descriptive error message. + """ + try: + if self.validate_etag_support: + if not self.module.params.get('validate_etag'): + self.oneview_client.connection.disable_etag_validation() + + result = self.execute_module() + + if "changed" not in result: + result['changed'] = False + + self.module.exit_json(**result) + + except HPOneViewException as exception: + self.module.fail_json(msg='; '.join(str(e) for e in exception.args)) + + def resource_absent(self, resource, method='delete'): + """ + Generic implementation of the absent state for the OneView resources. + It checks if the resource needs to be removed. + Args: + resource (dict): Resource to delete. + method (str): + Function of the OneView client that will be called for resource deletion. Usually delete or remove. + + Returns: + A dictionary with the expected arguments for the AnsibleModule.exit_json + + """ + if resource: + getattr(self.resource_client, method)(resource) + + return {"changed": True, "msg": self.MSG_DELETED} + else: + return {"changed": False, "msg": self.MSG_ALREADY_ABSENT} + + def get_by_name(self, name): + """ + Generic get by name implementation. + Args: + name: Resource name to search for. + + Returns: + The resource found or None. + """ + result = self.resource_client.get_by('name', name) + return result[0] if result else None + + def resource_present(self, resource, fact_name, create_method='create'): + """ + Generic implementation of the present state for the OneView resources. + It checks if the resource needs to be created or updated. + + Args: + resource (dict): + Resource to create or update. + fact_name (str): + Name of the fact returned to the Ansible. + create_method (str): + Function of the OneView client that will be called for resource creation. Usually create or add. + + Returns: + A dictionary with the expected arguments for the AnsibleModule.exit_json + """ + + changed = False + if "newName" in self.data: + self.data["name"] = self.data.pop("newName") + + if not resource: + resource = getattr(self.resource_client, create_method)(self.data) + msg = self.MSG_CREATED + changed = True + + else: + merged_data = resource.copy() + merged_data.update(self.data) + + if ResourceComparator.compare(resource, merged_data): + msg = self.MSG_ALREADY_PRESENT + else: + resource = self.resource_client.update(merged_data) + changed = True + msg = self.MSG_UPDATED + + return dict( + msg=msg, + changed=changed, + ansible_facts={fact_name: resource} + ) + + @staticmethod + def transform_list_to_dict(list_): + """ + Transforms a list into a dictionary, putting values as keys. + + Args: + list_: List of values + + Returns: + dict: dictionary built + """ + + ret = {} + + if not list_: + return ret + + for value in list_: + if isinstance(value, dict): + ret.update(value) + else: + ret[str(value)] = True + + return ret + + @staticmethod + def get_logger(mod_name): + """ + To activate logs, setup the environment var LOGFILE + e.g.: export LOGFILE=/tmp/ansible-oneview.log + + Args: + mod_name: module name + + Returns: Logger instance + """ + + logger = logging.getLogger(os.path.basename(mod_name)) + global LOGFILE + LOGFILE = os.environ.get('LOGFILE') + if not LOGFILE: + logger.addHandler(logging.NullHandler()) + else: + logging.basicConfig(level=logging.DEBUG, datefmt='%Y-%m-%d %H:%M:%S', + format='%(asctime)s %(levelname)s %(name)s %(message)s', + filename=LOGFILE, filemode='a') + return logger + + +class ResourceComparator(): + MSG_DIFF_AT_KEY = 'Difference found at key \'{0}\'. ' + + @staticmethod + def compare(first_resource, second_resource): + """ + Recursively compares dictionary contents equivalence, ignoring types and elements order. + Particularities of the comparison: + - Inexistent key = None + - These values are considered equal: None, empty, False + - Lists are compared value by value after a sort, if they have same size. + - Each element is converted to str before the comparison. + Args: + first_resource: first dictionary + second_resource: second dictionary + + Returns: + bool: True when equal, False when different. + """ + resource1 = deepcopy(first_resource) + resource2 = deepcopy(second_resource) + + debug_resources = "resource1 = {0}, resource2 = {1}".format(resource1, resource2) + + # The first resource is True / Not Null and the second resource is False / Null + if resource1 and not resource2: + logger.debug("resource1 and not resource2. " + debug_resources) + return False + + # Checks all keys in first dict against the second dict + for key in resource1.keys(): + if key not in resource2: + if resource1[key] is not None: + # Inexistent key is equivalent to exist with value None + logger.debug(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) + return False + # If both values are null, empty or False it will be considered equal. + elif not resource1[key] and not resource2[key]: + continue + elif isinstance(resource1[key], dict): + # recursive call + if not ResourceComparator.compare(resource1[key], resource2[key]): + logger.debug(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) + return False + elif isinstance(resource1[key], list): + # change comparison function to compare_list + if not ResourceComparator.compare_list(resource1[key], resource2[key]): + logger.debug(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) + return False + elif ResourceComparator._standardize_value(resource1[key]) != ResourceComparator._standardize_value( + resource2[key]): + logger.debug(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) + return False + + # Checks all keys in the second dict, looking for missing elements + for key in resource2.keys(): + if key not in resource1: + if resource2[key] is not None: + # Inexistent key is equivalent to exist with value None + logger.debug(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) + return False + + return True + + @staticmethod + def compare_list(first_resource, second_resource): + """ + Recursively compares lists contents equivalence, ignoring types and element orders. + Lists with same size are compared value by value after a sort, + each element is converted to str before the comparison. + Args: + first_resource: first list + second_resource: second list + + Returns: + True when equal; + False when different. + """ + + resource1 = deepcopy(first_resource) + resource2 = deepcopy(second_resource) + + debug_resources = "resource1 = {0}, resource2 = {1}".format(resource1, resource2) + + # The second list is null / empty / False + if not resource2: + logger.debug("resource 2 is null. " + debug_resources) + return False + + if len(resource1) != len(resource2): + logger.debug("resources have different length. " + debug_resources) + return False + + resource1 = sorted(resource1, key=ResourceComparator._str_sorted) + resource2 = sorted(resource2, key=ResourceComparator._str_sorted) + + for i, val in enumerate(resource1): + if isinstance(val, dict): + # change comparison function to compare dictionaries + if not ResourceComparator.compare(val, resource2[i]): + logger.debug("resources are different. " + debug_resources) + return False + elif isinstance(val, list): + # recursive call + if not ResourceComparator.compare_list(val, resource2[i]): + logger.debug("lists are different. " + debug_resources) + return False + elif ResourceComparator._standardize_value(val) != ResourceComparator._standardize_value(resource2[i]): + logger.debug("values are different. " + debug_resources) + return False + + # no differences found + return True + + @staticmethod + def _str_sorted(obj): + if isinstance(obj, dict): + return json.dumps(obj, sort_keys=True) + else: + return str(obj) + + @staticmethod + def _standardize_value(value): + """ + Convert value to string to enhance the comparison. + + Args: + value: Any object type. + + Returns: + str: Converted value. + """ + if isinstance(value, float) and value.is_integer(): + # Workaround to avoid erroneous comparison between int and float + # Removes zero from integer floats + value = int(value) + + return str(value) + + +class ResourceMerger(): + @staticmethod + def merge_list_by_key(original_list, updated_list, key, ignore_when_null=[]): + """ + Merge two lists by the key. It basically: + 1. Adds the items that are present on updated_list and are absent on original_list. + 2. Removes items that are absent on updated_list and are present on original_list. + 3. For all items that are in both lists, overwrites the values from the original item by the updated item. + + Args: + original_list: original list. + updated_list: list with changes. + key: unique identifier. + ignore_when_null: list with the keys from the updated items that should be ignored in the merge, if its + values are null. + Returns: + list: Lists merged. + """ + if not original_list: + return updated_list + + items_map = OrderedDict([(i[key], i.copy()) for i in original_list]) + + merged_items = OrderedDict() + + for item in updated_list: + item_key = item[key] + if item_key in items_map: + for ignored_key in ignore_when_null: + if ignored_key in item and not item[ignored_key]: + item.pop(ignored_key) + merged_items[item_key] = items_map[item_key].copy() + merged_items[item_key].update(item) + else: + merged_items[item_key] = item.copy() + + return [val for (_, val) in merged_items.items()] + + +class SPKeys(object): + ID = 'id' + NAME = 'name' + DEVICE_SLOT = 'deviceSlot' + CONNECTIONS = 'connections' + OS_DEPLOYMENT = 'osDeploymentSettings' + OS_DEPLOYMENT_URI = 'osDeploymentPlanUri' + ATTRIBUTES = 'osCustomAttributes' + SAN = 'sanStorage' + VOLUMES = 'volumeAttachments' + PATHS = 'storagePaths' + CONN_ID = 'connectionId' + BOOT = 'boot' + BIOS = 'bios' + BOOT_MODE = 'bootMode' + LOCAL_STORAGE = 'localStorage' + SAS_LOGICAL_JBODS = 'sasLogicalJBODs' + CONTROLLERS = 'controllers' + LOGICAL_DRIVES = 'logicalDrives' + SAS_LOGICAL_JBOD_URI = 'sasLogicalJBODUri' + SAS_LOGICAL_JBOD_ID = 'sasLogicalJBODId' + MODE = 'mode' + MAC_TYPE = 'macType' + MAC = 'mac' + SERIAL_NUMBER_TYPE = 'serialNumberType' + UUID = 'uuid' + SERIAL_NUMBER = 'serialNumber' + DRIVE_NUMBER = 'driveNumber' + WWPN_TYPE = 'wwpnType' + WWNN = 'wwnn' + WWPN = 'wwpn' + LUN_TYPE = 'lunType' + LUN = 'lun' + + +class ServerProfileMerger(object): + def merge_data(self, resource, data): + merged_data = deepcopy(resource) + merged_data.update(data) + + merged_data = self._merge_bios_and_boot(merged_data, resource, data) + merged_data = self._merge_connections(merged_data, resource, data) + merged_data = self._merge_san_storage(merged_data, data, resource) + merged_data = self._merge_os_deployment_settings(merged_data, resource, data) + merged_data = self._merge_local_storage(merged_data, resource, data) + + return merged_data + + def _merge_bios_and_boot(self, merged_data, resource, data): + if self._should_merge(data, resource, key=SPKeys.BIOS): + merged_data = self._merge_dict(merged_data, resource, data, key=SPKeys.BIOS) + if self._should_merge(data, resource, key=SPKeys.BOOT): + merged_data = self._merge_dict(merged_data, resource, data, key=SPKeys.BOOT) + if self._should_merge(data, resource, key=SPKeys.BOOT_MODE): + merged_data = self._merge_dict(merged_data, resource, data, key=SPKeys.BOOT_MODE) + return merged_data + + def _merge_connections(self, merged_data, resource, data): + if self._should_merge(data, resource, key=SPKeys.CONNECTIONS): + existing_connections = resource[SPKeys.CONNECTIONS] + params_connections = data[SPKeys.CONNECTIONS] + merged_data[SPKeys.CONNECTIONS] = ResourceMerger.merge_list_by_key(existing_connections, + params_connections, + key=SPKeys.ID) + + merged_data = self._merge_connections_boot(merged_data, resource) + return merged_data + + def _merge_connections_boot(self, merged_data, resource): + existing_connection_map = {x[SPKeys.ID]: x.copy() for x in resource[SPKeys.CONNECTIONS]} + for merged_connection in merged_data[SPKeys.CONNECTIONS]: + conn_id = merged_connection[SPKeys.ID] + existing_conn_has_boot = conn_id in existing_connection_map and SPKeys.BOOT in existing_connection_map[ + conn_id] + if existing_conn_has_boot and SPKeys.BOOT in merged_connection: + current_connection = existing_connection_map[conn_id] + boot_settings_merged = deepcopy(current_connection[SPKeys.BOOT]) + boot_settings_merged.update(merged_connection[SPKeys.BOOT]) + merged_connection[SPKeys.BOOT] = boot_settings_merged + return merged_data + + def _merge_san_storage(self, merged_data, data, resource): + if self._removed_data(data, resource, key=SPKeys.SAN): + merged_data[SPKeys.SAN] = dict(volumeAttachments=[], manageSanStorage=False) + elif self._should_merge(data, resource, key=SPKeys.SAN): + merged_data = self._merge_dict(merged_data, resource, data, key=SPKeys.SAN) + + merged_data = self._merge_san_volumes(merged_data, resource, data) + return merged_data + + def _merge_san_volumes(self, merged_data, resource, data): + if self._should_merge(data[SPKeys.SAN], resource[SPKeys.SAN], key=SPKeys.VOLUMES): + existing_volumes = resource[SPKeys.SAN][SPKeys.VOLUMES] + params_volumes = data[SPKeys.SAN][SPKeys.VOLUMES] + merged_volumes = ResourceMerger.merge_list_by_key(existing_volumes, params_volumes, key=SPKeys.ID) + merged_data[SPKeys.SAN][SPKeys.VOLUMES] = merged_volumes + + merged_data = self._merge_san_storage_paths(merged_data, resource) + return merged_data + + def _merge_san_storage_paths(self, merged_data, resource): + + existing_volumes_map = OrderedDict([(i[SPKeys.ID], i) for i in resource[SPKeys.SAN][SPKeys.VOLUMES]]) + merged_volumes = merged_data[SPKeys.SAN][SPKeys.VOLUMES] + for merged_volume in merged_volumes: + volume_id = merged_volume[SPKeys.ID] + if volume_id in existing_volumes_map: + if SPKeys.PATHS in merged_volume and SPKeys.PATHS in existing_volumes_map[volume_id]: + existent_paths = existing_volumes_map[volume_id][SPKeys.PATHS] + + paths_from_merged_volume = merged_volume[SPKeys.PATHS] + + merged_paths = ResourceMerger.merge_list_by_key(existent_paths, + paths_from_merged_volume, + key=SPKeys.CONN_ID) + + merged_volume[SPKeys.PATHS] = merged_paths + return merged_data + + def _merge_os_deployment_settings(self, merged_data, resource, data): + if self._should_merge(data, resource, key=SPKeys.OS_DEPLOYMENT): + merged_data = self._merge_dict(merged_data, resource, data, key=SPKeys.OS_DEPLOYMENT) + + merged_data = self._merge_os_deployment_custom_attr(merged_data, resource, data) + return merged_data + + def _merge_os_deployment_custom_attr(self, merged_data, resource, data): + if SPKeys.ATTRIBUTES in data[SPKeys.OS_DEPLOYMENT]: + existing_os_deployment = resource[SPKeys.OS_DEPLOYMENT] + params_os_deployment = data[SPKeys.OS_DEPLOYMENT] + merged_os_deployment = merged_data[SPKeys.OS_DEPLOYMENT] + + if self._removed_data(params_os_deployment, existing_os_deployment, key=SPKeys.ATTRIBUTES): + merged_os_deployment[SPKeys.ATTRIBUTES] = params_os_deployment[SPKeys.ATTRIBUTES] + else: + existing_attributes = existing_os_deployment[SPKeys.ATTRIBUTES] + params_attributes = params_os_deployment[SPKeys.ATTRIBUTES] + + if ResourceComparator.compare_list(existing_attributes, params_attributes): + merged_os_deployment[SPKeys.ATTRIBUTES] = existing_attributes + + return merged_data + + def _merge_local_storage(self, merged_data, resource, data): + if self._removed_data(data, resource, key=SPKeys.LOCAL_STORAGE): + merged_data[SPKeys.LOCAL_STORAGE] = dict(sasLogicalJBODs=[], controllers=[]) + elif self._should_merge(data, resource, key=SPKeys.LOCAL_STORAGE): + merged_data = self._merge_sas_logical_jbods(merged_data, resource, data) + merged_data = self._merge_controllers(merged_data, resource, data) + return merged_data + + def _merge_sas_logical_jbods(self, merged_data, resource, data): + if self._should_merge(data[SPKeys.LOCAL_STORAGE], resource[SPKeys.LOCAL_STORAGE], key=SPKeys.SAS_LOGICAL_JBODS): + existing_items = resource[SPKeys.LOCAL_STORAGE][SPKeys.SAS_LOGICAL_JBODS] + provided_items = merged_data[SPKeys.LOCAL_STORAGE][SPKeys.SAS_LOGICAL_JBODS] + merged_jbods = ResourceMerger.merge_list_by_key(existing_items, + provided_items, + key=SPKeys.ID, + ignore_when_null=[SPKeys.SAS_LOGICAL_JBOD_URI]) + merged_data[SPKeys.LOCAL_STORAGE][SPKeys.SAS_LOGICAL_JBODS] = merged_jbods + return merged_data + + def _merge_controllers(self, merged_data, resource, data): + if self._should_merge(data[SPKeys.LOCAL_STORAGE], resource[SPKeys.LOCAL_STORAGE], key=SPKeys.CONTROLLERS): + existing_items = resource[SPKeys.LOCAL_STORAGE][SPKeys.CONTROLLERS] + provided_items = merged_data[SPKeys.LOCAL_STORAGE][SPKeys.CONTROLLERS] + merged_controllers = ResourceMerger.merge_list_by_key(existing_items, + provided_items, + key=SPKeys.DEVICE_SLOT) + merged_data[SPKeys.LOCAL_STORAGE][SPKeys.CONTROLLERS] = merged_controllers + + merged_data = self._merge_controller_drives(merged_data, resource) + return merged_data + + def _merge_controller_drives(self, merged_data, resource): + for current_controller in merged_data[SPKeys.LOCAL_STORAGE][SPKeys.CONTROLLERS][:]: + for existing_controller in resource[SPKeys.LOCAL_STORAGE][SPKeys.CONTROLLERS][:]: + same_slot = current_controller.get(SPKeys.DEVICE_SLOT) == existing_controller.get(SPKeys.DEVICE_SLOT) + same_mode = existing_controller.get(SPKeys.MODE) == existing_controller.get(SPKeys.MODE) + if same_slot and same_mode and current_controller[SPKeys.LOGICAL_DRIVES]: + + key_merge = self._define_key_to_merge_drives(current_controller) + + if key_merge: + merged_drives = ResourceMerger.merge_list_by_key(existing_controller[SPKeys.LOGICAL_DRIVES], + current_controller[SPKeys.LOGICAL_DRIVES], + key=key_merge) + current_controller[SPKeys.LOGICAL_DRIVES] = merged_drives + return merged_data + + def _define_key_to_merge_drives(self, controller): + has_name = True + has_logical_jbod_id = True + for drive in controller[SPKeys.LOGICAL_DRIVES]: + if not drive.get(SPKeys.NAME): + has_name = False + if not drive.get(SPKeys.SAS_LOGICAL_JBOD_ID): + has_logical_jbod_id = False + + if has_name: + return SPKeys.NAME + elif has_logical_jbod_id: + return SPKeys.SAS_LOGICAL_JBOD_ID + return None + + def _removed_data(self, data, resource, key): + return key in data and not data[key] and key in resource + + def _should_merge(self, data, resource, key): + data_has_value = key in data and data[key] + existing_resource_has_value = key in resource and resource[key] + return data_has_value and existing_resource_has_value + + def _merge_dict(self, merged_data, resource, data, key): + if resource[key]: + merged_dict = deepcopy(resource[key]) + merged_dict.update(deepcopy(data[key])) + merged_data[key] = merged_dict + return merged_data + + +class ServerProfileReplaceNamesByUris(object): + SERVER_PROFILE_OS_DEPLOYMENT_NOT_FOUND = 'OS Deployment Plan not found: ' + SERVER_PROFILE_ENCLOSURE_GROUP_NOT_FOUND = 'Enclosure Group not found: ' + SERVER_PROFILE_NETWORK_NOT_FOUND = 'Network not found: ' + SERVER_HARDWARE_TYPE_NOT_FOUND = 'Server Hardware Type not found: ' + VOLUME_NOT_FOUND = 'Volume not found: ' + STORAGE_POOL_NOT_FOUND = 'Storage Pool not found: ' + STORAGE_SYSTEM_NOT_FOUND = 'Storage System not found: ' + INTERCONNECT_NOT_FOUND = 'Interconnect not found: ' + FIRMWARE_DRIVER_NOT_FOUND = 'Firmware Driver not found: ' + SAS_LOGICAL_JBOD_NOT_FOUND = 'SAS logical JBOD not found: ' + ENCLOSURE_NOT_FOUND = 'Enclosure not found: ' + + def replace(self, oneview_client, data): + self.oneview_client = oneview_client + self.__replace_os_deployment_name_by_uri(data) + self.__replace_enclosure_group_name_by_uri(data) + self.__replace_networks_name_by_uri(data) + self.__replace_server_hardware_type_name_by_uri(data) + self.__replace_volume_attachment_names_by_uri(data) + self.__replace_enclosure_name_by_uri(data) + self.__replace_interconnect_name_by_uri(data) + self.__replace_firmware_baseline_name_by_uri(data) + self.__replace_sas_logical_jbod_name_by_uri(data) + + def __replace_name_by_uri(self, data, attr_name, message, resource_client): + attr_uri = attr_name.replace("Name", "Uri") + if attr_name in data: + name = data.pop(attr_name) + resource_by_name = resource_client.get_by('name', name) + if not resource_by_name: + raise HPOneViewResourceNotFound(message + name) + data[attr_uri] = resource_by_name[0]['uri'] + + def __replace_os_deployment_name_by_uri(self, data): + if SPKeys.OS_DEPLOYMENT in data and data[SPKeys.OS_DEPLOYMENT]: + self.__replace_name_by_uri(data[SPKeys.OS_DEPLOYMENT], 'osDeploymentPlanName', + self.SERVER_PROFILE_OS_DEPLOYMENT_NOT_FOUND, + self.oneview_client.os_deployment_plans) + + def __replace_enclosure_group_name_by_uri(self, data): + self.__replace_name_by_uri(data, 'enclosureGroupName', self.SERVER_PROFILE_ENCLOSURE_GROUP_NOT_FOUND, + self.oneview_client.enclosure_groups) + + def __replace_networks_name_by_uri(self, data): + if SPKeys.CONNECTIONS in data and data[SPKeys.CONNECTIONS]: + for connection in data[SPKeys.CONNECTIONS]: + if 'networkName' in connection: + name = connection.pop('networkName', None) + connection['networkUri'] = self.__get_network_by_name(name)['uri'] + + def __replace_server_hardware_type_name_by_uri(self, data): + self.__replace_name_by_uri(data, 'serverHardwareTypeName', self.SERVER_HARDWARE_TYPE_NOT_FOUND, + self.oneview_client.server_hardware_types) + + def __replace_volume_attachment_names_by_uri(self, data): + volume_attachments = (data.get('sanStorage') or {}).get('volumeAttachments') or [] + if len(volume_attachments) > 0: + for volume in volume_attachments: + self.__replace_name_by_uri(volume, 'volumeName', self.VOLUME_NOT_FOUND, self.oneview_client.volumes) + self.__replace_name_by_uri(volume, 'volumeStoragePoolName', self.STORAGE_POOL_NOT_FOUND, + self.oneview_client.storage_pools) + self.__replace_name_by_uri(volume, 'volumeStorageSystemName', self.STORAGE_SYSTEM_NOT_FOUND, + self.oneview_client.storage_systems) + + def __replace_enclosure_name_by_uri(self, data): + self.__replace_name_by_uri(data, 'enclosureName', self.ENCLOSURE_NOT_FOUND, self.oneview_client.enclosures) + + def __replace_interconnect_name_by_uri(self, data): + connections = data.get('connections') or [] + if len(connections) > 0: + for connection in connections: + self.__replace_name_by_uri(connection, 'interconnectName', self.INTERCONNECT_NOT_FOUND, + self.oneview_client.interconnects) + + def __replace_firmware_baseline_name_by_uri(self, data): + firmware = data.get('firmware') or {} + self.__replace_name_by_uri(firmware, 'firmwareBaselineName', self.FIRMWARE_DRIVER_NOT_FOUND, + self.oneview_client.firmware_drivers) + + def __replace_sas_logical_jbod_name_by_uri(self, data): + sas_logical_jbods = (data.get('localStorage') or {}).get('sasLogicalJBODs') or [] + if len(sas_logical_jbods) > 0: + for jbod in sas_logical_jbods: + self.__replace_name_by_uri(jbod, 'sasLogicalJBODName', self.SAS_LOGICAL_JBOD_NOT_FOUND, + self.oneview_client.sas_logical_jbods) + + def __get_network_by_name(self, name): + fc_networks = self.oneview_client.fc_networks.get_by('name', name) + if fc_networks: + return fc_networks[0] + + ethernet_networks = self.oneview_client.ethernet_networks.get_by('name', name) + if not ethernet_networks: + raise HPOneViewResourceNotFound(self.SERVER_PROFILE_NETWORK_NOT_FOUND + name) + return ethernet_networks[0] diff --git a/lib/ansible/modules/cloud/hpe/__init__.py b/lib/ansible/modules/cloud/hpe/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/lib/ansible/modules/cloud/hpe/oneview_fc_network.py b/lib/ansible/modules/cloud/hpe/oneview_fc_network.py new file mode 100644 index 00000000000000..c422463b10c5b7 --- /dev/null +++ b/lib/ansible/modules/cloud/hpe/oneview_fc_network.py @@ -0,0 +1,121 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (2016-2017) Hewlett Packard Enterprise Development LP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['stableinterface'], + 'supported_by': 'curated'} + +DOCUMENTATION = ''' +--- +module: oneview_fc_network +short_description: Manage OneView Fibre Channel Network resources. +description: + - Provides an interface to manage Fibre Channel Network resources. Can create, update, and delete. +version_added: "2.4" +requirements: + - "python >= 2.7.9" + - "hpOneView >= 3.1.0" +author: "Bruno Souza (@bsouza)" +options: + state: + description: + - Indicates the desired state for the Fibre Channel Network resource. + C(present) will ensure data properties are compliant with OneView. + C(absent) will remove the resource from OneView, if it exists. + choices: ['present', 'absent'] + data: + description: + - List with the Fibre Channel Network properties. + required: true + +extends_documentation_fragment: + - oneview + - oneview.validateetag +''' + +EXAMPLES = ''' +- name: Ensure that the Fibre Channel Network is present using the default configuration + oneview_fc_network: + config: "{{ config_file_path }}" + state: present + data: + name: 'New FC Network' + +- name: Ensure that the Fibre Channel Network is present with fabricType 'DirectAttach' + oneview_fc_network: + config: "{{ config_file_path }}" + state: present + data: + name: 'New FC Network' + fabricType: 'DirectAttach' + +- name: Ensure that the Fibre Channel Network is absent + oneview_fc_network: + config: "{{ config_file_path }}" + state: absent + data: + name: 'New FC Network' +''' + +RETURN = ''' +fc_network: + description: Has the facts about the managed OneView FC Network. + returned: On state 'present'. Can be null. + type: complex + contains: FC Network data +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.oneview import OneViewModuleBase + + +class FcNetworkModule(OneViewModuleBase): + MSG_CREATED = 'FC Network created successfully.' + MSG_UPDATED = 'FC Network updated successfully.' + MSG_DELETED = 'FC Network deleted successfully.' + MSG_ALREADY_PRESENT = 'FC Network is already present.' + MSG_ALREADY_ABSENT = 'FC Network is already absent.' + RESOURCE_FACT_NAME = 'fc_network' + + def __init__(self): + + additional_arg_spec = dict(data=dict(required=True, type='dict'), + state=dict( + required=True, + choices=['present', 'absent'])) + + super(FcNetworkModule, self).__init__(additional_arg_spec=additional_arg_spec, + validate_etag_support=True) + + self.resource_client = self.oneview_client.fc_networks + + def execute_module(self): + resource = self.get_by_name(self.data['name']) + + if self.state == 'present': + return self.resource_present(resource, self.RESOURCE_FACT_NAME) + elif self.state == 'absent': + return self.resource_absent(resource) + + +def main(): + FcNetworkModule().run() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py b/lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py new file mode 100644 index 00000000000000..0bd985d578134e --- /dev/null +++ b/lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py @@ -0,0 +1,108 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (2016-2017) Hewlett Packard Enterprise Development LP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'curated', + 'metadata_version': '1.0'} + +DOCUMENTATION = ''' +--- +module: oneview_fc_network_facts +short_description: Retrieve the facts about one or more of the OneView Fibre Channel Networks. +description: + - Retrieve the facts about one or more of the Fibre Channel Networks from OneView. +version_added: "2.4" +requirements: + - "python >= 2.7.9" + - "hpOneView >= 2.0.1" +author: + "Mariana Kreisig (@marikrg)" +options: + name: + description: + - Fibre Channel Network name. + required: false + +extends_documentation_fragment: + - oneview + - oneview.factsparams +''' + +EXAMPLES = ''' +- name: Gather facts about all Fibre Channel Networks + oneview_fc_network_facts: + config: "{{ config_file_path }}" + +- debug: var=fc_networks + +- name: Gather paginated, filtered and sorted facts about Fibre Channel Networks + oneview_fc_network_facts: + config: "{{ config }}" + params: + start: 1 + count: 3 + sort: 'name:descending' + filter: 'fabricType=FabricAttach' +- debug: var=fc_networks + +- name: Gather facts about a Fibre Channel Network by name + oneview_fc_network_facts: + config: "{{ config_file_path }}" + name: network name + +- debug: var=fc_networks +''' + +RETURN = ''' +fc_networks: + description: Has all the OneView facts about the Fibre Channel Networks. + returned: Always, but can be null. + type: complex + contains: All parameters for the retrieved Fibre Channel Networks. +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.oneview import OneViewModuleBase + + +class FcNetworkFactsModule(OneViewModuleBase): + def __init__(self): + + argument_spec = dict( + name=dict(required=False, type='str'), + params=dict(required=False, type='dict') + ) + + super(FcNetworkFactsModule, self).__init__(additional_arg_spec=argument_spec) + + def execute_module(self): + + if self.module.params['name']: + fc_networks = self.oneview_client.fc_networks.get_by('name', self.module.params['name']) + else: + fc_networks = self.oneview_client.fc_networks.get_all(**self.facts_params) + + return dict(changed=False, ansible_facts=dict(fc_networks=fc_networks)) + + +def main(): + FcNetworkFactsModule().run() + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/utils/module_docs_fragments/oneview.py b/lib/ansible/utils/module_docs_fragments/oneview.py new file mode 100644 index 00000000000000..63f4de701b1a2b --- /dev/null +++ b/lib/ansible/utils/module_docs_fragments/oneview.py @@ -0,0 +1,62 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (2016-2017) Hewlett Packard Enterprise Development LP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +class ModuleDocFragment(object): + + # OneView doc fragment + DOCUMENTATION = ''' +options: + config: + description: + - Path to a .json configuration file containing the OneView client configuration. + The configuration file is optional. If the file path is not provided, the configuration will be loaded from + environment variables. + required: false + +notes: + - "A sample configuration file for the config parameter can be found at: + U(https://github.com/HewlettPackard/oneview-ansible/blob/master/examples/oneview_config-rename.json)" + - "Check how to use environment variables for configuration at: + U(https://github.com/HewlettPackard/oneview-ansible#environment-variables)" + - "Additional Playbooks for the HPE OneView Ansible modules can be found at: + U(https://github.com/HewlettPackard/oneview-ansible/tree/master/examples)" + ''' + + VALIDATEETAG = ''' +options: + validate_etag: + description: + - When the ETag Validation is enabled, the request will be conditionally processed only if the current ETag + for the resource matches the ETag provided in the data. + default: true + choices: ['true', 'false'] +''' + + FACTSPARAMS = ''' +options: + params: + description: + - List of params to delimit, filter and sort the list of resources. + - "params allowed: + C(start): The first item to return, using 0-based indexing. + C(count): The number of resources to return. + C(filter): A general filter/query string to narrow the list of items returned. + C(sort): The sort order of the returned data set." + required: false +''' diff --git a/test/runner/requirements/units.txt b/test/runner/requirements/units.txt index dced151ca9f862..9e8e6d2c5fce07 100644 --- a/test/runner/requirements/units.txt +++ b/test/runner/requirements/units.txt @@ -17,6 +17,7 @@ setuptools > 0.6 # pytest-xdist installed via requirements does not work with ve unittest2 ; python_version < '2.7' netaddr ipaddress +hpOneView # requirements for F5 specific modules f5-sdk ; python_version >= '2.7' diff --git a/test/units/modules/cloud/hpe/hpe_test_utils.py b/test/units/modules/cloud/hpe/hpe_test_utils.py new file mode 100644 index 00000000000000..3faeb0355a2f95 --- /dev/null +++ b/test/units/modules/cloud/hpe/hpe_test_utils.py @@ -0,0 +1,136 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (2016-2017) Hewlett Packard Enterprise Development LP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import importlib +import yaml +from mock import Mock, patch +from oneview_module_loader import ONEVIEW_MODULE_UTILS_PATH +from hpOneView.oneview_client import OneViewClient + + +class OneViewBaseTestCase(object): + mock_ov_client_from_json_file = None + testing_class = None + mock_ansible_module = None + mock_ov_client = None + testing_module = None + EXAMPLES = None + + def configure_mocks(self, test_case, testing_class): + """ + Preload mocked OneViewClient instance and AnsibleModule + Args: + test_case (object): class instance (self) that are inheriting from OneViewBaseTestCase + testing_class (object): class being tested + """ + self.testing_class = testing_class + + # Define OneView Client Mock (FILE) + patcher_json_file = patch.object(OneViewClient, 'from_json_file') + test_case.addCleanup(patcher_json_file.stop) + self.mock_ov_client_from_json_file = patcher_json_file.start() + + # Define OneView Client Mock + self.mock_ov_client = self.mock_ov_client_from_json_file.return_value + + # Define Ansible Module Mock + patcher_ansible = patch(ONEVIEW_MODULE_UTILS_PATH + '.AnsibleModule') + test_case.addCleanup(patcher_ansible.stop) + mock_ansible_module = patcher_ansible.start() + self.mock_ansible_module = Mock() + mock_ansible_module.return_value = self.mock_ansible_module + + self.__set_module_examples() + + def test_main_function_should_call_run_method(self): + self.mock_ansible_module.params = {'config': 'config.json'} + + main_func = getattr(self.testing_module, 'main') + + with patch.object(self.testing_class, "run") as mock_run: + main_func() + mock_run.assert_called_once() + + def __set_module_examples(self): + self.testing_module = importlib.import_module(self.testing_class.__module__) + + try: + # Load scenarios from module examples (Also checks if it is a valid yaml) + self.EXAMPLES = yaml.load(self.testing_module.EXAMPLES, yaml.SafeLoader) + + except yaml.scanner.ScannerError: + message = "Something went wrong while parsing yaml from {}.EXAMPLES".format(self.testing_class.__module__) + raise Exception(message) + + +class FactsParamsTestCase(OneViewBaseTestCase): + """ + FactsParamsTestCase has common test for classes that support pass additional + parameters when retrieving all resources. + """ + + def configure_client_mock(self, resorce_client): + """ + Args: + resorce_client: Resource client that is being called + """ + self.resource_client = resorce_client + + def __validations(self): + if not self.testing_class: + raise Exception("Mocks are not configured, you must call 'configure_mocks' before running this test.") + + if not self.resource_client: + raise Exception( + "Mock for the client not configured, you must call 'configure_client_mock' before running this test.") + + def test_should_get_all_using_filters(self): + self.__validations() + self.resource_client.get_all.return_value = [] + + params_get_all_with_filters = dict( + config='config.json', + name=None, + params={ + 'start': 1, + 'count': 3, + 'sort': 'name:descending', + 'filter': 'purpose=General', + 'query': 'imported eq true' + }) + self.mock_ansible_module.params = params_get_all_with_filters + + self.testing_class().run() + + self.resource_client.get_all.assert_called_once_with(start=1, count=3, sort='name:descending', + filter='purpose=General', + query='imported eq true') + + def test_should_get_all_without_params(self): + self.__validations() + self.resource_client.get_all.return_value = [] + + params_get_all_with_filters = dict( + config='config.json', + name=None + ) + self.mock_ansible_module.params = params_get_all_with_filters + + self.testing_class().run() + + self.resource_client.get_all.assert_called_once_with() diff --git a/test/units/modules/cloud/hpe/oneview_module_loader.py b/test/units/modules/cloud/hpe/oneview_module_loader.py new file mode 100644 index 00000000000000..0dd9ca0efa31d1 --- /dev/null +++ b/test/units/modules/cloud/hpe/oneview_module_loader.py @@ -0,0 +1,35 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (2016-2017) Hewlett Packard Enterprise Development LP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +This module was created because the code in this repository is shared with Ansible Core. +So, to avoid merging issues, and maintaining the tests code equal, we create a unique file to +configure the imports that change from ansible.modules.cloud.hpe.one repository to another. +""" + +ONEVIEW_MODULE_UTILS_PATH = 'ansible.module_utils.oneview' + +from ansible.module_utils.oneview import (HPOneViewException, + HPOneViewTaskError, + OneViewModuleBase, + SPKeys, + ServerProfileMerger, + ServerProfileReplaceNamesByUris, + ResourceComparator) + +from ansible.modules.cloud.hpe.oneview_fc_network import FcNetworkModule +from ansible.modules.cloud.hpe.oneview_fc_network_facts import FcNetworkFactsModule diff --git a/test/units/modules/cloud/hpe/test_oneview_fc_network.py b/test/units/modules/cloud/hpe/test_oneview_fc_network.py new file mode 100644 index 00000000000000..287e422c06f41d --- /dev/null +++ b/test/units/modules/cloud/hpe/test_oneview_fc_network.py @@ -0,0 +1,134 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (2016-2017) Hewlett Packard Enterprise Development LP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import unittest + +from oneview_module_loader import FcNetworkModule +from hpe_test_utils import OneViewBaseTestCase + +FAKE_MSG_ERROR = 'Fake message error' + +DEFAULT_FC_NETWORK_TEMPLATE = dict( + name='New FC Network 2', + autoLoginRedistribution=True, + fabricType='FabricAttach' +) + +PARAMS_FOR_PRESENT = dict( + config='config.json', + state='present', + data=dict(name=DEFAULT_FC_NETWORK_TEMPLATE['name']) +) + +PARAMS_WITH_CHANGES = dict( + config='config.json', + state='present', + data=dict(name=DEFAULT_FC_NETWORK_TEMPLATE['name'], + newName="New Name", + fabricType='DirectAttach') +) + +PARAMS_FOR_ABSENT = dict( + config='config.json', + state='absent', + data=dict(name=DEFAULT_FC_NETWORK_TEMPLATE['name']) +) + + +class FcNetworkModuleSpec(unittest.TestCase, + OneViewBaseTestCase): + """ + OneViewBaseTestCase provides the mocks used in this test case + """ + + def setUp(self): + self.configure_mocks(self, FcNetworkModule) + self.resource = self.mock_ov_client.fc_networks + + def test_should_create_new_fc_network(self): + self.resource.get_by.return_value = [] + self.resource.create.return_value = DEFAULT_FC_NETWORK_TEMPLATE + + self.mock_ansible_module.params = PARAMS_FOR_PRESENT + + FcNetworkModule().run() + + self.mock_ansible_module.exit_json.assert_called_once_with( + changed=True, + msg=FcNetworkModule.MSG_CREATED, + ansible_facts=dict(fc_network=DEFAULT_FC_NETWORK_TEMPLATE) + ) + + def test_should_not_update_when_data_is_equals(self): + self.resource.get_by.return_value = [DEFAULT_FC_NETWORK_TEMPLATE] + + self.mock_ansible_module.params = PARAMS_FOR_PRESENT + + FcNetworkModule().run() + + self.mock_ansible_module.exit_json.assert_called_once_with( + changed=False, + msg=FcNetworkModule.MSG_ALREADY_PRESENT, + ansible_facts=dict(fc_network=DEFAULT_FC_NETWORK_TEMPLATE) + ) + + def test_update_when_data_has_modified_attributes(self): + data_merged = DEFAULT_FC_NETWORK_TEMPLATE.copy() + + data_merged['fabricType'] = 'DirectAttach' + + self.resource.get_by.return_value = [DEFAULT_FC_NETWORK_TEMPLATE] + self.resource.update.return_value = data_merged + + self.mock_ansible_module.params = PARAMS_WITH_CHANGES + + FcNetworkModule().run() + + self.mock_ansible_module.exit_json.assert_called_once_with( + changed=True, + msg=FcNetworkModule.MSG_UPDATED, + ansible_facts=dict(fc_network=data_merged) + ) + + def test_should_remove_fc_network(self): + self.resource.get_by.return_value = [DEFAULT_FC_NETWORK_TEMPLATE] + + self.mock_ansible_module.params = PARAMS_FOR_ABSENT + + FcNetworkModule().run() + + self.mock_ansible_module.exit_json.assert_called_once_with( + changed=True, + msg=FcNetworkModule.MSG_DELETED + ) + + def test_should_do_nothing_when_fc_network_not_exist(self): + self.resource.get_by.return_value = [] + + self.mock_ansible_module.params = PARAMS_FOR_ABSENT + + FcNetworkModule().run() + + self.mock_ansible_module.exit_json.assert_called_once_with( + changed=False, + msg=FcNetworkModule.MSG_ALREADY_ABSENT + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/units/modules/cloud/hpe/test_oneview_fc_network_facts.py b/test/units/modules/cloud/hpe/test_oneview_fc_network_facts.py new file mode 100644 index 00000000000000..959b3623e3a202 --- /dev/null +++ b/test/units/modules/cloud/hpe/test_oneview_fc_network_facts.py @@ -0,0 +1,73 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (2016-2017) Hewlett Packard Enterprise Development LP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import unittest + +from oneview_module_loader import FcNetworkFactsModule +from hpe_test_utils import FactsParamsTestCase + +ERROR_MSG = 'Fake message error' + +PARAMS_GET_ALL = dict( + config='config.json', + name=None +) + +PARAMS_GET_BY_NAME = dict( + config='config.json', + name="Test FC Network" +) + +PRESENT_NETWORKS = [{ + "name": "Test FC Network", + "uri": "/rest/fc-networks/c6bf9af9-48e7-4236-b08a-77684dc258a5" +}] + + +class FcNetworkFactsSpec(unittest.TestCase, + FactsParamsTestCase): + def setUp(self): + self.configure_mocks(self, FcNetworkFactsModule) + self.fc_networks = self.mock_ov_client.fc_networks + FactsParamsTestCase.configure_client_mock(self, self.fc_networks) + + def test_should_get_all_fc_networks(self): + self.fc_networks.get_all.return_value = PRESENT_NETWORKS + self.mock_ansible_module.params = PARAMS_GET_ALL + + FcNetworkFactsModule().run() + + self.mock_ansible_module.exit_json.assert_called_once_with( + changed=False, + ansible_facts=dict(fc_networks=PRESENT_NETWORKS) + ) + + def test_should_get_fc_network_by_name(self): + self.fc_networks.get_by.return_value = PRESENT_NETWORKS + self.mock_ansible_module.params = PARAMS_GET_BY_NAME + + FcNetworkFactsModule().run() + + self.mock_ansible_module.exit_json.assert_called_once_with( + changed=False, + ansible_facts=dict(fc_networks=PRESENT_NETWORKS) + ) + + +if __name__ == '__main__': + unittest.main() From 7969abedf03288f766ceb4beccf33130278651e9 Mon Sep 17 00:00:00 2001 From: fgbulsoni Date: Mon, 26 Jun 2017 15:08:31 -0300 Subject: [PATCH 02/19] Adding mocks for oneview module imports and removing shebangs from files that offended CI rules. --- lib/ansible/module_utils/oneview.py | 5 +++-- lib/ansible/modules/cloud/hpe/oneview_fc_network.py | 2 +- .../modules/cloud/hpe/oneview_fc_network_facts.py | 2 +- lib/ansible/utils/module_docs_fragments/oneview.py | 1 - test/runner/requirements/units.txt | 2 +- test/units/modules/cloud/hpe/hpe_test_utils.py | 1 - test/units/modules/cloud/hpe/oneview_module_loader.py | 11 +++++++++-- .../modules/cloud/hpe/test_oneview_fc_network.py | 1 - .../cloud/hpe/test_oneview_fc_network_facts.py | 1 - 9 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/ansible/module_utils/oneview.py b/lib/ansible/module_utils/oneview.py index 89b8d55330d2cb..d24ce893437e52 100644 --- a/lib/ansible/module_utils/oneview.py +++ b/lib/ansible/module_utils/oneview.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (2016-2017) Hewlett Packard Enterprise Development LP @@ -521,7 +520,9 @@ def _merge_connections(self, merged_data, resource, data): return merged_data def _merge_connections_boot(self, merged_data, resource): - existing_connection_map = {x[SPKeys.ID]: x.copy() for x in resource[SPKeys.CONNECTIONS]} + existing_connection_map = {} + for x in resource[SPKeys.CONNECTIONS]: + existing_connection_map[x[SPKeys.ID]] = x.copy() for merged_connection in merged_data[SPKeys.CONNECTIONS]: conn_id = merged_connection[SPKeys.ID] existing_conn_has_boot = conn_id in existing_connection_map and SPKeys.BOOT in existing_connection_map[ diff --git a/lib/ansible/modules/cloud/hpe/oneview_fc_network.py b/lib/ansible/modules/cloud/hpe/oneview_fc_network.py index c422463b10c5b7..c787fd5a4d82ad 100644 --- a/lib/ansible/modules/cloud/hpe/oneview_fc_network.py +++ b/lib/ansible/modules/cloud/hpe/oneview_fc_network.py @@ -18,7 +18,7 @@ ANSIBLE_METADATA = {'metadata_version': '1.0', 'status': ['stableinterface'], - 'supported_by': 'curated'} + 'supported_by': 'community'} DOCUMENTATION = ''' --- diff --git a/lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py b/lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py index 0bd985d578134e..03ab78414f6f80 100644 --- a/lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py +++ b/lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py @@ -17,7 +17,7 @@ # along with this program. If not, see . ANSIBLE_METADATA = {'status': ['stableinterface'], - 'supported_by': 'curated', + 'supported_by': 'community', 'metadata_version': '1.0'} DOCUMENTATION = ''' diff --git a/lib/ansible/utils/module_docs_fragments/oneview.py b/lib/ansible/utils/module_docs_fragments/oneview.py index 63f4de701b1a2b..ab4274f4c2f601 100644 --- a/lib/ansible/utils/module_docs_fragments/oneview.py +++ b/lib/ansible/utils/module_docs_fragments/oneview.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (2016-2017) Hewlett Packard Enterprise Development LP diff --git a/test/runner/requirements/units.txt b/test/runner/requirements/units.txt index 9e8e6d2c5fce07..8dd8634dcc1a4b 100644 --- a/test/runner/requirements/units.txt +++ b/test/runner/requirements/units.txt @@ -1,5 +1,6 @@ boto boto3 +hpOneView placebo cryptography pycrypto @@ -17,7 +18,6 @@ setuptools > 0.6 # pytest-xdist installed via requirements does not work with ve unittest2 ; python_version < '2.7' netaddr ipaddress -hpOneView # requirements for F5 specific modules f5-sdk ; python_version >= '2.7' diff --git a/test/units/modules/cloud/hpe/hpe_test_utils.py b/test/units/modules/cloud/hpe/hpe_test_utils.py index 3faeb0355a2f95..d7b1936be51891 100644 --- a/test/units/modules/cloud/hpe/hpe_test_utils.py +++ b/test/units/modules/cloud/hpe/hpe_test_utils.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (2016-2017) Hewlett Packard Enterprise Development LP diff --git a/test/units/modules/cloud/hpe/oneview_module_loader.py b/test/units/modules/cloud/hpe/oneview_module_loader.py index 0dd9ca0efa31d1..7c5bcc93d7c502 100644 --- a/test/units/modules/cloud/hpe/oneview_module_loader.py +++ b/test/units/modules/cloud/hpe/oneview_module_loader.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (2016-2017) Hewlett Packard Enterprise Development LP @@ -21,8 +20,16 @@ configure the imports that change from ansible.modules.cloud.hpe.one repository to another. """ -ONEVIEW_MODULE_UTILS_PATH = 'ansible.module_utils.oneview' +import sys +from ansible.compat.tests.mock import patch, Mock +sys.modules['hpOneView'] = Mock() +sys.modules['hpOneView.oneview_client'] = Mock() +sys.modules['hpOneView.exceptions'] = Mock() +sys.modules['future'] = Mock() +sys.modules['__future__'] = Mock() +sys.modules['OrderedDict'] = Mock() +ONEVIEW_MODULE_UTILS_PATH = 'ansible.module_utils.oneview' from ansible.module_utils.oneview import (HPOneViewException, HPOneViewTaskError, OneViewModuleBase, diff --git a/test/units/modules/cloud/hpe/test_oneview_fc_network.py b/test/units/modules/cloud/hpe/test_oneview_fc_network.py index 287e422c06f41d..07b14b027678fb 100644 --- a/test/units/modules/cloud/hpe/test_oneview_fc_network.py +++ b/test/units/modules/cloud/hpe/test_oneview_fc_network.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (2016-2017) Hewlett Packard Enterprise Development LP diff --git a/test/units/modules/cloud/hpe/test_oneview_fc_network_facts.py b/test/units/modules/cloud/hpe/test_oneview_fc_network_facts.py index 959b3623e3a202..211da822a7d9b0 100644 --- a/test/units/modules/cloud/hpe/test_oneview_fc_network_facts.py +++ b/test/units/modules/cloud/hpe/test_oneview_fc_network_facts.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (2016-2017) Hewlett Packard Enterprise Development LP From 8af214c50cc578ca94ed2ba8f030edc902cf2649 Mon Sep 17 00:00:00 2001 From: fgbulsoni Date: Wed, 28 Jun 2017 09:37:07 -0300 Subject: [PATCH 03/19] Changing OrderedDict imports and calls syntax for unit tests to pass --- lib/ansible/module_utils/oneview.py | 8 ++++---- test/units/modules/cloud/hpe/oneview_module_loader.py | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/ansible/module_utils/oneview.py b/lib/ansible/module_utils/oneview.py index d24ce893437e52..286a8d0941e790 100644 --- a/lib/ansible/module_utils/oneview.py +++ b/lib/ansible/module_utils/oneview.py @@ -24,9 +24,9 @@ import json import logging import os +import collections from ansible.module_utils.basic import AnsibleModule from copy import deepcopy -from collections import OrderedDict standard_library.install_aliases() logger = logging.getLogger(__name__) @@ -433,9 +433,9 @@ def merge_list_by_key(original_list, updated_list, key, ignore_when_null=[]): if not original_list: return updated_list - items_map = OrderedDict([(i[key], i.copy()) for i in original_list]) + items_map = collections.OrderedDict([(i[key], i.copy()) for i in original_list]) - merged_items = OrderedDict() + merged_items = collections.OrderedDict() for item in updated_list: item_key = item[key] @@ -555,7 +555,7 @@ def _merge_san_volumes(self, merged_data, resource, data): def _merge_san_storage_paths(self, merged_data, resource): - existing_volumes_map = OrderedDict([(i[SPKeys.ID], i) for i in resource[SPKeys.SAN][SPKeys.VOLUMES]]) + existing_volumes_map = collections.OrderedDict([(i[SPKeys.ID], i) for i in resource[SPKeys.SAN][SPKeys.VOLUMES]]) merged_volumes = merged_data[SPKeys.SAN][SPKeys.VOLUMES] for merged_volume in merged_volumes: volume_id = merged_volume[SPKeys.ID] diff --git a/test/units/modules/cloud/hpe/oneview_module_loader.py b/test/units/modules/cloud/hpe/oneview_module_loader.py index 7c5bcc93d7c502..c329f346033c33 100644 --- a/test/units/modules/cloud/hpe/oneview_module_loader.py +++ b/test/units/modules/cloud/hpe/oneview_module_loader.py @@ -27,7 +27,6 @@ sys.modules['hpOneView.exceptions'] = Mock() sys.modules['future'] = Mock() sys.modules['__future__'] = Mock() -sys.modules['OrderedDict'] = Mock() ONEVIEW_MODULE_UTILS_PATH = 'ansible.module_utils.oneview' from ansible.module_utils.oneview import (HPOneViewException, From b11506b3755cae95c7260aee18e3d1d44ec43b41 Mon Sep 17 00:00:00 2001 From: fgbulsoni Date: Mon, 3 Jul 2017 08:21:37 -0300 Subject: [PATCH 04/19] Changing import of unittest to use ansible.compat.tests --- test/units/modules/cloud/hpe/oneview_module_loader.py | 5 ----- test/units/modules/cloud/hpe/test_oneview_fc_network.py | 3 +-- .../units/modules/cloud/hpe/test_oneview_fc_network_facts.py | 3 +-- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/test/units/modules/cloud/hpe/oneview_module_loader.py b/test/units/modules/cloud/hpe/oneview_module_loader.py index c329f346033c33..89a552967f8ed3 100644 --- a/test/units/modules/cloud/hpe/oneview_module_loader.py +++ b/test/units/modules/cloud/hpe/oneview_module_loader.py @@ -14,11 +14,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -""" -This module was created because the code in this repository is shared with Ansible Core. -So, to avoid merging issues, and maintaining the tests code equal, we create a unique file to -configure the imports that change from ansible.modules.cloud.hpe.one repository to another. -""" import sys from ansible.compat.tests.mock import patch, Mock diff --git a/test/units/modules/cloud/hpe/test_oneview_fc_network.py b/test/units/modules/cloud/hpe/test_oneview_fc_network.py index 07b14b027678fb..6d6d1e3876fbb7 100644 --- a/test/units/modules/cloud/hpe/test_oneview_fc_network.py +++ b/test/units/modules/cloud/hpe/test_oneview_fc_network.py @@ -15,8 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import unittest - +from ansible.compat.tests import unittest from oneview_module_loader import FcNetworkModule from hpe_test_utils import OneViewBaseTestCase diff --git a/test/units/modules/cloud/hpe/test_oneview_fc_network_facts.py b/test/units/modules/cloud/hpe/test_oneview_fc_network_facts.py index 211da822a7d9b0..b8b456cc5d007f 100644 --- a/test/units/modules/cloud/hpe/test_oneview_fc_network_facts.py +++ b/test/units/modules/cloud/hpe/test_oneview_fc_network_facts.py @@ -15,8 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import unittest - +from ansible.compat.tests import unittest from oneview_module_loader import FcNetworkFactsModule from hpe_test_utils import FactsParamsTestCase From b4740225aaf63b3d4954b88cc24f30ed7b419e76 Mon Sep 17 00:00:00 2001 From: fgbulsoni Date: Mon, 3 Jul 2017 14:56:49 -0300 Subject: [PATCH 05/19] Changing status on ansible metadata from stableinterface to preview --- lib/ansible/modules/cloud/hpe/oneview_fc_network.py | 2 +- lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ansible/modules/cloud/hpe/oneview_fc_network.py b/lib/ansible/modules/cloud/hpe/oneview_fc_network.py index c787fd5a4d82ad..373387f42d4410 100644 --- a/lib/ansible/modules/cloud/hpe/oneview_fc_network.py +++ b/lib/ansible/modules/cloud/hpe/oneview_fc_network.py @@ -17,7 +17,7 @@ # along with this program. If not, see . ANSIBLE_METADATA = {'metadata_version': '1.0', - 'status': ['stableinterface'], + 'status': ['preview'], 'supported_by': 'community'} DOCUMENTATION = ''' diff --git a/lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py b/lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py index 03ab78414f6f80..576a014ba829ee 100644 --- a/lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py +++ b/lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py @@ -16,9 +16,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -ANSIBLE_METADATA = {'status': ['stableinterface'], - 'supported_by': 'community', - 'metadata_version': '1.0'} +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community'} DOCUMENTATION = ''' --- From ce27f80c96d017401b38278d4bf838ef2796e18c Mon Sep 17 00:00:00 2001 From: fgbulsoni Date: Fri, 7 Jul 2017 09:36:12 -0300 Subject: [PATCH 06/19] Removed FcNetworkFacts module so the PR features only one module --- .../cloud/hpe/oneview_fc_network_facts.py | 108 ------------------ .../cloud/hpe/oneview_module_loader.py | 1 - .../hpe/test_oneview_fc_network_facts.py | 71 ------------ 3 files changed, 180 deletions(-) delete mode 100644 lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py delete mode 100644 test/units/modules/cloud/hpe/test_oneview_fc_network_facts.py diff --git a/lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py b/lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py deleted file mode 100644 index 576a014ba829ee..00000000000000 --- a/lib/ansible/modules/cloud/hpe/oneview_fc_network_facts.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright (2016-2017) Hewlett Packard Enterprise Development LP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -ANSIBLE_METADATA = {'metadata_version': '1.0', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: oneview_fc_network_facts -short_description: Retrieve the facts about one or more of the OneView Fibre Channel Networks. -description: - - Retrieve the facts about one or more of the Fibre Channel Networks from OneView. -version_added: "2.4" -requirements: - - "python >= 2.7.9" - - "hpOneView >= 2.0.1" -author: - "Mariana Kreisig (@marikrg)" -options: - name: - description: - - Fibre Channel Network name. - required: false - -extends_documentation_fragment: - - oneview - - oneview.factsparams -''' - -EXAMPLES = ''' -- name: Gather facts about all Fibre Channel Networks - oneview_fc_network_facts: - config: "{{ config_file_path }}" - -- debug: var=fc_networks - -- name: Gather paginated, filtered and sorted facts about Fibre Channel Networks - oneview_fc_network_facts: - config: "{{ config }}" - params: - start: 1 - count: 3 - sort: 'name:descending' - filter: 'fabricType=FabricAttach' -- debug: var=fc_networks - -- name: Gather facts about a Fibre Channel Network by name - oneview_fc_network_facts: - config: "{{ config_file_path }}" - name: network name - -- debug: var=fc_networks -''' - -RETURN = ''' -fc_networks: - description: Has all the OneView facts about the Fibre Channel Networks. - returned: Always, but can be null. - type: complex - contains: All parameters for the retrieved Fibre Channel Networks. -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.oneview import OneViewModuleBase - - -class FcNetworkFactsModule(OneViewModuleBase): - def __init__(self): - - argument_spec = dict( - name=dict(required=False, type='str'), - params=dict(required=False, type='dict') - ) - - super(FcNetworkFactsModule, self).__init__(additional_arg_spec=argument_spec) - - def execute_module(self): - - if self.module.params['name']: - fc_networks = self.oneview_client.fc_networks.get_by('name', self.module.params['name']) - else: - fc_networks = self.oneview_client.fc_networks.get_all(**self.facts_params) - - return dict(changed=False, ansible_facts=dict(fc_networks=fc_networks)) - - -def main(): - FcNetworkFactsModule().run() - - -if __name__ == '__main__': - main() diff --git a/test/units/modules/cloud/hpe/oneview_module_loader.py b/test/units/modules/cloud/hpe/oneview_module_loader.py index 89a552967f8ed3..d09889a5625b23 100644 --- a/test/units/modules/cloud/hpe/oneview_module_loader.py +++ b/test/units/modules/cloud/hpe/oneview_module_loader.py @@ -33,4 +33,3 @@ ResourceComparator) from ansible.modules.cloud.hpe.oneview_fc_network import FcNetworkModule -from ansible.modules.cloud.hpe.oneview_fc_network_facts import FcNetworkFactsModule diff --git a/test/units/modules/cloud/hpe/test_oneview_fc_network_facts.py b/test/units/modules/cloud/hpe/test_oneview_fc_network_facts.py deleted file mode 100644 index b8b456cc5d007f..00000000000000 --- a/test/units/modules/cloud/hpe/test_oneview_fc_network_facts.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (2016-2017) Hewlett Packard Enterprise Development LP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from ansible.compat.tests import unittest -from oneview_module_loader import FcNetworkFactsModule -from hpe_test_utils import FactsParamsTestCase - -ERROR_MSG = 'Fake message error' - -PARAMS_GET_ALL = dict( - config='config.json', - name=None -) - -PARAMS_GET_BY_NAME = dict( - config='config.json', - name="Test FC Network" -) - -PRESENT_NETWORKS = [{ - "name": "Test FC Network", - "uri": "/rest/fc-networks/c6bf9af9-48e7-4236-b08a-77684dc258a5" -}] - - -class FcNetworkFactsSpec(unittest.TestCase, - FactsParamsTestCase): - def setUp(self): - self.configure_mocks(self, FcNetworkFactsModule) - self.fc_networks = self.mock_ov_client.fc_networks - FactsParamsTestCase.configure_client_mock(self, self.fc_networks) - - def test_should_get_all_fc_networks(self): - self.fc_networks.get_all.return_value = PRESENT_NETWORKS - self.mock_ansible_module.params = PARAMS_GET_ALL - - FcNetworkFactsModule().run() - - self.mock_ansible_module.exit_json.assert_called_once_with( - changed=False, - ansible_facts=dict(fc_networks=PRESENT_NETWORKS) - ) - - def test_should_get_fc_network_by_name(self): - self.fc_networks.get_by.return_value = PRESENT_NETWORKS - self.mock_ansible_module.params = PARAMS_GET_BY_NAME - - FcNetworkFactsModule().run() - - self.mock_ansible_module.exit_json.assert_called_once_with( - changed=False, - ansible_facts=dict(fc_networks=PRESENT_NETWORKS) - ) - - -if __name__ == '__main__': - unittest.main() From 513c098d502646851dca12243beafce6c9d33827 Mon Sep 17 00:00:00 2001 From: fgbulsoni Date: Thu, 13 Jul 2017 09:15:17 -0300 Subject: [PATCH 07/19] Removed hpOneView requirement from unit tests and fixed importlib issues for 2.6 compatibility --- lib/ansible/modules/cloud/hpe/oneview_fc_network.py | 2 +- test/runner/requirements/units.txt | 1 - test/units/modules/cloud/hpe/hpe_test_utils.py | 6 ++++-- test/units/modules/cloud/hpe/test_oneview_fc_network.py | 4 ---- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/ansible/modules/cloud/hpe/oneview_fc_network.py b/lib/ansible/modules/cloud/hpe/oneview_fc_network.py index 373387f42d4410..988ea84b90211e 100644 --- a/lib/ansible/modules/cloud/hpe/oneview_fc_network.py +++ b/lib/ansible/modules/cloud/hpe/oneview_fc_network.py @@ -109,7 +109,7 @@ def execute_module(self): if self.state == 'present': return self.resource_present(resource, self.RESOURCE_FACT_NAME) - elif self.state == 'absent': + else: return self.resource_absent(resource) diff --git a/test/runner/requirements/units.txt b/test/runner/requirements/units.txt index 8dd8634dcc1a4b..dced151ca9f862 100644 --- a/test/runner/requirements/units.txt +++ b/test/runner/requirements/units.txt @@ -1,6 +1,5 @@ boto boto3 -hpOneView placebo cryptography pycrypto diff --git a/test/units/modules/cloud/hpe/hpe_test_utils.py b/test/units/modules/cloud/hpe/hpe_test_utils.py index d7b1936be51891..7b945ace103f94 100644 --- a/test/units/modules/cloud/hpe/hpe_test_utils.py +++ b/test/units/modules/cloud/hpe/hpe_test_utils.py @@ -15,7 +15,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import importlib import yaml from mock import Mock, patch from oneview_module_loader import ONEVIEW_MODULE_UTILS_PATH @@ -66,7 +65,10 @@ def test_main_function_should_call_run_method(self): mock_run.assert_called_once() def __set_module_examples(self): - self.testing_module = importlib.import_module(self.testing_class.__module__) + # Load scenarios from module examples (Also checks if it is a valid yaml) + ansible = __import__('ansible') + testing_module = self.testing_class.__module__.split('.')[-1] + self.testing_module = getattr(ansible.modules.cloud.hpe, testing_module) try: # Load scenarios from module examples (Also checks if it is a valid yaml) diff --git a/test/units/modules/cloud/hpe/test_oneview_fc_network.py b/test/units/modules/cloud/hpe/test_oneview_fc_network.py index 6d6d1e3876fbb7..6249469a2414cb 100644 --- a/test/units/modules/cloud/hpe/test_oneview_fc_network.py +++ b/test/units/modules/cloud/hpe/test_oneview_fc_network.py @@ -126,7 +126,3 @@ def test_should_do_nothing_when_fc_network_not_exist(self): changed=False, msg=FcNetworkModule.MSG_ALREADY_ABSENT ) - - -if __name__ == '__main__': - unittest.main() From 4da165fde014e3af2b5955855e081659dabe0242 Mon Sep 17 00:00:00 2001 From: fgbulsoni Date: Thu, 13 Jul 2017 10:42:00 -0300 Subject: [PATCH 08/19] Skipping future import in case it is not available --- lib/ansible/module_utils/oneview.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/ansible/module_utils/oneview.py b/lib/ansible/module_utils/oneview.py index 286a8d0941e790..2e12821d329acc 100644 --- a/lib/ansible/module_utils/oneview.py +++ b/lib/ansible/module_utils/oneview.py @@ -20,7 +20,12 @@ print_function, unicode_literals) -from future import standard_library +try: + from future import standard_library + HAS_FUTURE = True +except: + HAS_FUTURE = False + pass import json import logging import os @@ -28,7 +33,9 @@ from ansible.module_utils.basic import AnsibleModule from copy import deepcopy -standard_library.install_aliases() +if HAS_FUTURE: + standard_library.install_aliases() + logger = logging.getLogger(__name__) try: @@ -50,6 +57,7 @@ class OneViewModuleBase(object): MSG_ALREADY_PRESENT = 'Resource is already present.' MSG_ALREADY_ABSENT = 'Resource is already absent.' HPE_ONEVIEW_SDK_REQUIRED = 'HPE OneView Python SDK is required for this module.' + FUTURE_PACKAGE_REQUIRED = 'The Future Python package is required for this module.' ONEVIEW_COMMON_ARGS = dict( config=dict(required=False, type='str') @@ -107,6 +115,8 @@ def __build_argument_spec(self, additional_arg_spec, validate_etag_support): def __check_hpe_oneview_sdk(self): if not HAS_HPE_ONEVIEW: self.module.fail_json(msg=self.HPE_ONEVIEW_SDK_REQUIRED) + if not HAS_FUTURE: + self.module.fail_json(msg=self.FUTURE_PACKAGE_REQUIRED) def __create_oneview_client(self): if not self.module.params['config']: @@ -554,8 +564,8 @@ def _merge_san_volumes(self, merged_data, resource, data): return merged_data def _merge_san_storage_paths(self, merged_data, resource): - - existing_volumes_map = collections.OrderedDict([(i[SPKeys.ID], i) for i in resource[SPKeys.SAN][SPKeys.VOLUMES]]) + existing_volumes_map = collections.OrderedDict([(i[SPKeys.ID], i) for i in resource[ + SPKeys.SAN][SPKeys.VOLUMES]]) merged_volumes = merged_data[SPKeys.SAN][SPKeys.VOLUMES] for merged_volume in merged_volumes: volume_id = merged_volume[SPKeys.ID] From a39a5bc9bf5d24ca31a6f9daaf0de52dcef3c7a8 Mon Sep 17 00:00:00 2001 From: Felipe Bulsoni Date: Fri, 14 Jul 2017 01:01:22 -0300 Subject: [PATCH 09/19] Moving module from cloud to remote_management and changing author to match the current maintainer of the code --- .../modules/{cloud => remote_management}/hpe/__init__.py | 0 .../{cloud => remote_management}/hpe/oneview_fc_network.py | 2 +- test/units/modules/remote_management/__init__.py | 0 .../modules/{cloud => remote_management}/hpe/hpe_test_utils.py | 2 +- .../{cloud => remote_management}/hpe/oneview_module_loader.py | 2 +- .../{cloud => remote_management}/hpe/test_oneview_fc_network.py | 0 6 files changed, 3 insertions(+), 3 deletions(-) rename lib/ansible/modules/{cloud => remote_management}/hpe/__init__.py (100%) rename lib/ansible/modules/{cloud => remote_management}/hpe/oneview_fc_network.py (99%) create mode 100644 test/units/modules/remote_management/__init__.py rename test/units/modules/{cloud => remote_management}/hpe/hpe_test_utils.py (98%) rename test/units/modules/{cloud => remote_management}/hpe/oneview_module_loader.py (94%) rename test/units/modules/{cloud => remote_management}/hpe/test_oneview_fc_network.py (100%) diff --git a/lib/ansible/modules/cloud/hpe/__init__.py b/lib/ansible/modules/remote_management/hpe/__init__.py similarity index 100% rename from lib/ansible/modules/cloud/hpe/__init__.py rename to lib/ansible/modules/remote_management/hpe/__init__.py diff --git a/lib/ansible/modules/cloud/hpe/oneview_fc_network.py b/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py similarity index 99% rename from lib/ansible/modules/cloud/hpe/oneview_fc_network.py rename to lib/ansible/modules/remote_management/hpe/oneview_fc_network.py index 988ea84b90211e..b5e6c07fd32abe 100644 --- a/lib/ansible/modules/cloud/hpe/oneview_fc_network.py +++ b/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py @@ -30,7 +30,7 @@ requirements: - "python >= 2.7.9" - "hpOneView >= 3.1.0" -author: "Bruno Souza (@bsouza)" +author: "Felipe Bulsoni (@fgbulsoni)" options: state: description: diff --git a/test/units/modules/remote_management/__init__.py b/test/units/modules/remote_management/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/units/modules/cloud/hpe/hpe_test_utils.py b/test/units/modules/remote_management/hpe/hpe_test_utils.py similarity index 98% rename from test/units/modules/cloud/hpe/hpe_test_utils.py rename to test/units/modules/remote_management/hpe/hpe_test_utils.py index 7b945ace103f94..4e2d53a13de56d 100644 --- a/test/units/modules/cloud/hpe/hpe_test_utils.py +++ b/test/units/modules/remote_management/hpe/hpe_test_utils.py @@ -68,7 +68,7 @@ def __set_module_examples(self): # Load scenarios from module examples (Also checks if it is a valid yaml) ansible = __import__('ansible') testing_module = self.testing_class.__module__.split('.')[-1] - self.testing_module = getattr(ansible.modules.cloud.hpe, testing_module) + self.testing_module = getattr(ansible.modules.remote_management.hpe, testing_module) try: # Load scenarios from module examples (Also checks if it is a valid yaml) diff --git a/test/units/modules/cloud/hpe/oneview_module_loader.py b/test/units/modules/remote_management/hpe/oneview_module_loader.py similarity index 94% rename from test/units/modules/cloud/hpe/oneview_module_loader.py rename to test/units/modules/remote_management/hpe/oneview_module_loader.py index d09889a5625b23..3a87eb8c34545b 100644 --- a/test/units/modules/cloud/hpe/oneview_module_loader.py +++ b/test/units/modules/remote_management/hpe/oneview_module_loader.py @@ -32,4 +32,4 @@ ServerProfileReplaceNamesByUris, ResourceComparator) -from ansible.modules.cloud.hpe.oneview_fc_network import FcNetworkModule +from ansible.modules.remote_management.hpe.oneview_fc_network import FcNetworkModule diff --git a/test/units/modules/cloud/hpe/test_oneview_fc_network.py b/test/units/modules/remote_management/hpe/test_oneview_fc_network.py similarity index 100% rename from test/units/modules/cloud/hpe/test_oneview_fc_network.py rename to test/units/modules/remote_management/hpe/test_oneview_fc_network.py From e783df0139ef55cedc66a3113ab6126a5920bf2e Mon Sep 17 00:00:00 2001 From: fgbulsoni Date: Fri, 28 Jul 2017 14:49:58 -0300 Subject: [PATCH 10/19] Changed dual underscore convention for single underscore in all private methods, changed documentation to reflect sphinx more correctly, removed logger, fixed many point commented on the review --- lib/ansible/module_utils/oneview.py | 362 ++++++++---------- .../hpe/oneview_fc_network.py | 3 +- 2 files changed, 162 insertions(+), 203 deletions(-) diff --git a/lib/ansible/module_utils/oneview.py b/lib/ansible/module_utils/oneview.py index 2e12821d329acc..cfac2d0d3b48b5 100644 --- a/lib/ansible/module_utils/oneview.py +++ b/lib/ansible/module_utils/oneview.py @@ -1,42 +1,45 @@ -# -*- coding: utf-8 -*- +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. # # Copyright (2016-2017) Hewlett Packard Enterprise Development LP +# All rights reserved. # -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: # -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. # -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import (absolute_import, division, - print_function, - unicode_literals) + print_function) -try: - from future import standard_library - HAS_FUTURE = True -except: - HAS_FUTURE = False - pass +import collections import json import logging import os -import collections -from ansible.module_utils.basic import AnsibleModule +import traceback from copy import deepcopy -if HAS_FUTURE: - standard_library.install_aliases() - -logger = logging.getLogger(__name__) +from ansible.module_utils import six +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native try: from hpOneView.oneview_client import OneViewClient @@ -50,6 +53,28 @@ HAS_HPE_ONEVIEW = False +def transform_list_to_dict(list_): + """ + Transforms a list into a dictionary, putting values as keys. + + :arg list list_: List of values + :return: dict: dictionary built +""" + + ret = {} + + if not list_: + return ret + + for value in list_: + if isinstance(value, dict): + ret.update(value) + else: + ret[to_native(value, errors='surrogate_or_strict')] = True + + return ret + + class OneViewModuleBase(object): MSG_CREATED = 'Resource created successfully.' MSG_UPDATED = 'Resource updated successfully.' @@ -76,17 +101,15 @@ def __init__(self, additional_arg_spec=None, validate_etag_support=False): """ OneViewModuleBase constructor. - Args: - additional_arg_spec (dict): Additional argument spec definition. - validate_etag_support (bool): Enables support to eTag validation. - """ - - argument_spec = self.__build_argument_spec(additional_arg_spec, validate_etag_support) + :arg dict additional_arg_spec: Additional argument spec definition. + :arg bool validate_etag_support: Enables support to eTag validation. +""" + argument_spec = self._build_argument_spec(additional_arg_spec, validate_etag_support) self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) - self.__check_hpe_oneview_sdk() - self.__create_oneview_client() + self._check_hpe_oneview_sdk() + self._create_oneview_client() self.state = self.module.params.get('state') self.data = self.module.params.get('data') @@ -95,11 +118,11 @@ def __init__(self, additional_arg_spec=None, validate_etag_support=False): self.facts_params = self.module.params.get('params') or {} # Preload options as dict - used by facts - self.options = self.transform_list_to_dict(self.module.params.get('options')) + self.options = transform_list_to_dict(self.module.params.get('options')) self.validate_etag_support = validate_etag_support - def __build_argument_spec(self, additional_arg_spec, validate_etag_support): + def _build_argument_spec(self, additional_arg_spec, validate_etag_support): merged_arg_spec = dict() merged_arg_spec.update(self.ONEVIEW_COMMON_ARGS) @@ -112,13 +135,11 @@ def __build_argument_spec(self, additional_arg_spec, validate_etag_support): return merged_arg_spec - def __check_hpe_oneview_sdk(self): + def _check_hpe_oneview_sdk(self): if not HAS_HPE_ONEVIEW: self.module.fail_json(msg=self.HPE_ONEVIEW_SDK_REQUIRED) - if not HAS_FUTURE: - self.module.fail_json(msg=self.FUTURE_PACKAGE_REQUIRED) - def __create_oneview_client(self): + def _create_oneview_client(self): if not self.module.params['config']: self.oneview_client = OneViewClient.from_environment_variables() else: @@ -130,19 +151,20 @@ def execute_module(self): This method is called from the run method. It should contains the module logic - Returns: - dict: - It must return a dictionary with the attributes for the module result, - such as ansible_facts, msg and changed. - """ + :return: dict: It must return a dictionary with the attributes for the module result, + such as ansible_facts, msg and changed. +""" raise HPOneViewException("execute_module not implemented") def run(self): """ Common implementation of the OneView run modules. + It calls the inheritor 'execute_module' function and sends the return to the Ansible. + It handles any HPOneViewException in order to signal a failure to Ansible, with a descriptive error message. - """ + +""" try: if self.validate_etag_support: if not self.module.params.get('validate_etag'): @@ -156,21 +178,20 @@ def run(self): self.module.exit_json(**result) except HPOneViewException as exception: - self.module.fail_json(msg='; '.join(str(e) for e in exception.args)) + error_msg = '; '.join(to_native(e) for e in exception.args) + self.module.fail_json(msg=error_msg, exception=traceback.format_exc()) def resource_absent(self, resource, method='delete'): """ Generic implementation of the absent state for the OneView resources. - It checks if the resource needs to be removed. - Args: - resource (dict): Resource to delete. - method (str): - Function of the OneView client that will be called for resource deletion. Usually delete or remove. - Returns: - A dictionary with the expected arguments for the AnsibleModule.exit_json + It checks if the resource needs to be removed. - """ + :arg dict resource: Resource to delete. + :arg str method: Function of the OneView client that will be called for resource deletion. + Usually delete or remove. + :return: A dictionary with the expected arguments for the AnsibleModule.exit_json +""" if resource: getattr(self.resource_client, method)(resource) @@ -181,31 +202,26 @@ def resource_absent(self, resource, method='delete'): def get_by_name(self, name): """ Generic get by name implementation. - Args: - name: Resource name to search for. - Returns: - The resource found or None. - """ + :arg str name: Resource name to search for. + + :return: The resource found or None. +""" result = self.resource_client.get_by('name', name) return result[0] if result else None def resource_present(self, resource, fact_name, create_method='create'): """ Generic implementation of the present state for the OneView resources. - It checks if the resource needs to be created or updated. - Args: - resource (dict): - Resource to create or update. - fact_name (str): - Name of the fact returned to the Ansible. - create_method (str): - Function of the OneView client that will be called for resource creation. Usually create or add. + It checks if the resource needs to be created or updated. - Returns: - A dictionary with the expected arguments for the AnsibleModule.exit_json - """ + :arg dict resource: Resource to create or update. + :arg str fact_name: Name of the fact returned to the Ansible. + :arg str create_method: Function of the OneView client that will be called for resource creation. + Usually create or add. + :return: A dictionary with the expected arguments for the AnsibleModule.exit_json +""" changed = False if "newName" in self.data: @@ -233,54 +249,6 @@ def resource_present(self, resource, fact_name, create_method='create'): ansible_facts={fact_name: resource} ) - @staticmethod - def transform_list_to_dict(list_): - """ - Transforms a list into a dictionary, putting values as keys. - - Args: - list_: List of values - - Returns: - dict: dictionary built - """ - - ret = {} - - if not list_: - return ret - - for value in list_: - if isinstance(value, dict): - ret.update(value) - else: - ret[str(value)] = True - - return ret - - @staticmethod - def get_logger(mod_name): - """ - To activate logs, setup the environment var LOGFILE - e.g.: export LOGFILE=/tmp/ansible-oneview.log - - Args: - mod_name: module name - - Returns: Logger instance - """ - - logger = logging.getLogger(os.path.basename(mod_name)) - global LOGFILE - LOGFILE = os.environ.get('LOGFILE') - if not LOGFILE: - logger.addHandler(logging.NullHandler()) - else: - logging.basicConfig(level=logging.DEBUG, datefmt='%Y-%m-%d %H:%M:%S', - format='%(asctime)s %(levelname)s %(name)s %(message)s', - filename=LOGFILE, filemode='a') - return logger - class ResourceComparator(): MSG_DIFF_AT_KEY = 'Difference found at key \'{0}\'. ' @@ -294,21 +262,18 @@ def compare(first_resource, second_resource): - These values are considered equal: None, empty, False - Lists are compared value by value after a sort, if they have same size. - Each element is converted to str before the comparison. - Args: - first_resource: first dictionary - second_resource: second dictionary - - Returns: - bool: True when equal, False when different. - """ + :arg dict first_resource: first dictionary + :arg dict second_resource: second dictionary + :return: bool: True when equal, False when different. +""" resource1 = deepcopy(first_resource) resource2 = deepcopy(second_resource) - debug_resources = "resource1 = {0}, resource2 = {1}".format(resource1, resource2) + # debug_resources = "resource1 = {0}, resource2 = {1}".format(resource1, resource2) # The first resource is True / Not Null and the second resource is False / Null if resource1 and not resource2: - logger.debug("resource1 and not resource2. " + debug_resources) + # self.log("resource1 and not resource2. " + debug_resources) return False # Checks all keys in first dict against the second dict @@ -316,7 +281,7 @@ def compare(first_resource, second_resource): if key not in resource2: if resource1[key] is not None: # Inexistent key is equivalent to exist with value None - logger.debug(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) + # self.log(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) return False # If both values are null, empty or False it will be considered equal. elif not resource1[key] and not resource2[key]: @@ -324,16 +289,16 @@ def compare(first_resource, second_resource): elif isinstance(resource1[key], dict): # recursive call if not ResourceComparator.compare(resource1[key], resource2[key]): - logger.debug(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) + # self.log(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) return False elif isinstance(resource1[key], list): # change comparison function to compare_list if not ResourceComparator.compare_list(resource1[key], resource2[key]): - logger.debug(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) + # self.log(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) return False elif ResourceComparator._standardize_value(resource1[key]) != ResourceComparator._standardize_value( resource2[key]): - logger.debug(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) + # self.log(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) return False # Checks all keys in the second dict, looking for missing elements @@ -341,7 +306,7 @@ def compare(first_resource, second_resource): if key not in resource1: if resource2[key] is not None: # Inexistent key is equivalent to exist with value None - logger.debug(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) + # self.log(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) return False return True @@ -352,27 +317,23 @@ def compare_list(first_resource, second_resource): Recursively compares lists contents equivalence, ignoring types and element orders. Lists with same size are compared value by value after a sort, each element is converted to str before the comparison. - Args: - first_resource: first list - second_resource: second list - - Returns: - True when equal; - False when different. - """ + :arg list first_resource: first list + :arg list second_resource: second list + :return: True when equal; False when different. +""" resource1 = deepcopy(first_resource) resource2 = deepcopy(second_resource) - debug_resources = "resource1 = {0}, resource2 = {1}".format(resource1, resource2) + # debug_resources = "resource1 = {0}, resource2 = {1}".format(resource1, resource2) # The second list is null / empty / False if not resource2: - logger.debug("resource 2 is null. " + debug_resources) + # self.log("resource 2 is null. " + debug_resources) return False if len(resource1) != len(resource2): - logger.debug("resources have different length. " + debug_resources) + # self.log("resources have different length. " + debug_resources) return False resource1 = sorted(resource1, key=ResourceComparator._str_sorted) @@ -382,15 +343,15 @@ def compare_list(first_resource, second_resource): if isinstance(val, dict): # change comparison function to compare dictionaries if not ResourceComparator.compare(val, resource2[i]): - logger.debug("resources are different. " + debug_resources) + # self.log("resources are different. " + debug_resources) return False elif isinstance(val, list): # recursive call if not ResourceComparator.compare_list(val, resource2[i]): - logger.debug("lists are different. " + debug_resources) + # self.log("lists are different. " + debug_resources) return False elif ResourceComparator._standardize_value(val) != ResourceComparator._standardize_value(resource2[i]): - logger.debug("values are different. " + debug_resources) + # self.log("values are different. " + debug_resources) return False # no differences found @@ -408,12 +369,10 @@ def _standardize_value(value): """ Convert value to string to enhance the comparison. - Args: - value: Any object type. + :arg value: Any object type. - Returns: - str: Converted value. - """ + :return: str: Converted value. +""" if isinstance(value, float) and value.is_integer(): # Workaround to avoid erroneous comparison between int and float # Removes zero from integer floats @@ -427,19 +386,20 @@ class ResourceMerger(): def merge_list_by_key(original_list, updated_list, key, ignore_when_null=[]): """ Merge two lists by the key. It basically: + 1. Adds the items that are present on updated_list and are absent on original_list. + 2. Removes items that are absent on updated_list and are present on original_list. + 3. For all items that are in both lists, overwrites the values from the original item by the updated item. - Args: - original_list: original list. - updated_list: list with changes. - key: unique identifier. - ignore_when_null: list with the keys from the updated items that should be ignored in the merge, if its - values are null. - Returns: - list: Lists merged. - """ + :arg list original_list: original list. + :arg list updated_list: list with changes. + :arg str key: unique identifier. + :arg list ignore_when_null: list with the keys from the updated items that should be ignored in the merge, + if its values are null. + :return: list: Lists merged. +""" if not original_list: return updated_list @@ -699,17 +659,17 @@ class ServerProfileReplaceNamesByUris(object): def replace(self, oneview_client, data): self.oneview_client = oneview_client - self.__replace_os_deployment_name_by_uri(data) - self.__replace_enclosure_group_name_by_uri(data) - self.__replace_networks_name_by_uri(data) - self.__replace_server_hardware_type_name_by_uri(data) - self.__replace_volume_attachment_names_by_uri(data) - self.__replace_enclosure_name_by_uri(data) - self.__replace_interconnect_name_by_uri(data) - self.__replace_firmware_baseline_name_by_uri(data) - self.__replace_sas_logical_jbod_name_by_uri(data) - - def __replace_name_by_uri(self, data, attr_name, message, resource_client): + self._replace_os_deployment_name_by_uri(data) + self._replace_enclosure_group_name_by_uri(data) + self._replace_networks_name_by_uri(data) + self._replace_server_hardware_type_name_by_uri(data) + self._replace_volume_attachment_names_by_uri(data) + self._replace_enclosure_name_by_uri(data) + self._replace_interconnect_name_by_uri(data) + self._replace_firmware_baseline_name_by_uri(data) + self._replace_sas_logical_jbod_name_by_uri(data) + + def _replace_name_by_uri(self, data, attr_name, message, resource_client): attr_uri = attr_name.replace("Name", "Uri") if attr_name in data: name = data.pop(attr_name) @@ -718,60 +678,60 @@ def __replace_name_by_uri(self, data, attr_name, message, resource_client): raise HPOneViewResourceNotFound(message + name) data[attr_uri] = resource_by_name[0]['uri'] - def __replace_os_deployment_name_by_uri(self, data): + def _replace_os_deployment_name_by_uri(self, data): if SPKeys.OS_DEPLOYMENT in data and data[SPKeys.OS_DEPLOYMENT]: - self.__replace_name_by_uri(data[SPKeys.OS_DEPLOYMENT], 'osDeploymentPlanName', - self.SERVER_PROFILE_OS_DEPLOYMENT_NOT_FOUND, - self.oneview_client.os_deployment_plans) + self._replace_name_by_uri(data[SPKeys.OS_DEPLOYMENT], 'osDeploymentPlanName', + self.SERVER_PROFILE_OS_DEPLOYMENT_NOT_FOUND, + self.oneview_client.os_deployment_plans) - def __replace_enclosure_group_name_by_uri(self, data): - self.__replace_name_by_uri(data, 'enclosureGroupName', self.SERVER_PROFILE_ENCLOSURE_GROUP_NOT_FOUND, - self.oneview_client.enclosure_groups) + def _replace_enclosure_group_name_by_uri(self, data): + self._replace_name_by_uri(data, 'enclosureGroupName', self.SERVER_PROFILE_ENCLOSURE_GROUP_NOT_FOUND, + self.oneview_client.enclosure_groups) - def __replace_networks_name_by_uri(self, data): + def _replace_networks_name_by_uri(self, data): if SPKeys.CONNECTIONS in data and data[SPKeys.CONNECTIONS]: for connection in data[SPKeys.CONNECTIONS]: if 'networkName' in connection: name = connection.pop('networkName', None) - connection['networkUri'] = self.__get_network_by_name(name)['uri'] + connection['networkUri'] = self._get_network_by_name(name)['uri'] - def __replace_server_hardware_type_name_by_uri(self, data): - self.__replace_name_by_uri(data, 'serverHardwareTypeName', self.SERVER_HARDWARE_TYPE_NOT_FOUND, - self.oneview_client.server_hardware_types) + def _replace_server_hardware_type_name_by_uri(self, data): + self._replace_name_by_uri(data, 'serverHardwareTypeName', self.SERVER_HARDWARE_TYPE_NOT_FOUND, + self.oneview_client.server_hardware_types) - def __replace_volume_attachment_names_by_uri(self, data): + def _replace_volume_attachment_names_by_uri(self, data): volume_attachments = (data.get('sanStorage') or {}).get('volumeAttachments') or [] if len(volume_attachments) > 0: for volume in volume_attachments: - self.__replace_name_by_uri(volume, 'volumeName', self.VOLUME_NOT_FOUND, self.oneview_client.volumes) - self.__replace_name_by_uri(volume, 'volumeStoragePoolName', self.STORAGE_POOL_NOT_FOUND, - self.oneview_client.storage_pools) - self.__replace_name_by_uri(volume, 'volumeStorageSystemName', self.STORAGE_SYSTEM_NOT_FOUND, - self.oneview_client.storage_systems) + self._replace_name_by_uri(volume, 'volumeName', self.VOLUME_NOT_FOUND, self.oneview_client.volumes) + self._replace_name_by_uri(volume, 'volumeStoragePoolName', self.STORAGE_POOL_NOT_FOUND, + self.oneview_client.storage_pools) + self._replace_name_by_uri(volume, 'volumeStorageSystemName', self.STORAGE_SYSTEM_NOT_FOUND, + self.oneview_client.storage_systems) - def __replace_enclosure_name_by_uri(self, data): - self.__replace_name_by_uri(data, 'enclosureName', self.ENCLOSURE_NOT_FOUND, self.oneview_client.enclosures) + def _replace_enclosure_name_by_uri(self, data): + self._replace_name_by_uri(data, 'enclosureName', self.ENCLOSURE_NOT_FOUND, self.oneview_client.enclosures) - def __replace_interconnect_name_by_uri(self, data): + def _replace_interconnect_name_by_uri(self, data): connections = data.get('connections') or [] if len(connections) > 0: for connection in connections: - self.__replace_name_by_uri(connection, 'interconnectName', self.INTERCONNECT_NOT_FOUND, - self.oneview_client.interconnects) + self._replace_name_by_uri(connection, 'interconnectName', self.INTERCONNECT_NOT_FOUND, + self.oneview_client.interconnects) - def __replace_firmware_baseline_name_by_uri(self, data): + def _replace_firmware_baseline_name_by_uri(self, data): firmware = data.get('firmware') or {} - self.__replace_name_by_uri(firmware, 'firmwareBaselineName', self.FIRMWARE_DRIVER_NOT_FOUND, - self.oneview_client.firmware_drivers) + self._replace_name_by_uri(firmware, 'firmwareBaselineName', self.FIRMWARE_DRIVER_NOT_FOUND, + self.oneview_client.firmware_drivers) - def __replace_sas_logical_jbod_name_by_uri(self, data): + def _replace_sas_logical_jbod_name_by_uri(self, data): sas_logical_jbods = (data.get('localStorage') or {}).get('sasLogicalJBODs') or [] if len(sas_logical_jbods) > 0: for jbod in sas_logical_jbods: - self.__replace_name_by_uri(jbod, 'sasLogicalJBODName', self.SAS_LOGICAL_JBOD_NOT_FOUND, - self.oneview_client.sas_logical_jbods) + self._replace_name_by_uri(jbod, 'sasLogicalJBODName', self.SAS_LOGICAL_JBOD_NOT_FOUND, + self.oneview_client.sas_logical_jbods) - def __get_network_by_name(self, name): + def _get_network_by_name(self, name): fc_networks = self.oneview_client.fc_networks.get_by('name', name) if fc_networks: return fc_networks[0] diff --git a/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py b/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py index b5e6c07fd32abe..fb1cd155092c02 100644 --- a/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py +++ b/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py @@ -76,8 +76,7 @@ fc_network: description: Has the facts about the managed OneView FC Network. returned: On state 'present'. Can be null. - type: complex - contains: FC Network data + type: dict ''' from ansible.module_utils.basic import AnsibleModule From 171986ad8dfe5d4abe616b27ea0c475e9390c45a Mon Sep 17 00:00:00 2001 From: fgbulsoni Date: Tue, 1 Aug 2017 10:59:13 -0300 Subject: [PATCH 11/19] Adding documentation mentioning to check notes section when trying to setup env vars --- lib/ansible/utils/module_docs_fragments/oneview.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/ansible/utils/module_docs_fragments/oneview.py b/lib/ansible/utils/module_docs_fragments/oneview.py index ab4274f4c2f601..03198ceb142f3c 100644 --- a/lib/ansible/utils/module_docs_fragments/oneview.py +++ b/lib/ansible/utils/module_docs_fragments/oneview.py @@ -24,8 +24,9 @@ class ModuleDocFragment(object): config: description: - Path to a .json configuration file containing the OneView client configuration. - The configuration file is optional. If the file path is not provided, the configuration will be loaded from - environment variables. + The configuration file is optional and when used should be present in the host running the ansible commands. + If the file path is not provided, the configuration will be loaded from environment variables. + For links to example configuration files or how to use the environment variables verify the notes section. required: false notes: From 40c637678aa2c5f0ce0edb7f8c2f542c47de0e23 Mon Sep 17 00:00:00 2001 From: fgbulsoni Date: Tue, 1 Aug 2017 12:01:48 -0300 Subject: [PATCH 12/19] Adding __metadata__ and changing __future__ import to try to fix code according to boilerplate test --- lib/ansible/module_utils/oneview.py | 7 +++---- .../modules/remote_management/hpe/oneview_fc_network.py | 3 +++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/ansible/module_utils/oneview.py b/lib/ansible/module_utils/oneview.py index cfac2d0d3b48b5..6ba5257667e5bb 100644 --- a/lib/ansible/module_utils/oneview.py +++ b/lib/ansible/module_utils/oneview.py @@ -26,9 +26,7 @@ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from __future__ import (absolute_import, - division, - print_function) +from __future__ import (absolute_import, division, print_function) import collections import json @@ -418,7 +416,8 @@ def merge_list_by_key(original_list, updated_list, key, ignore_when_null=[]): else: merged_items[item_key] = item.copy() - return [val for (_, val) in merged_items.items()] + return list(merged_items.values()) + # return [val for (_, val) in merged_items.items()] class SPKeys(object): diff --git a/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py b/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py index fb1cd155092c02..2d51a3b3dc4720 100644 --- a/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py +++ b/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py @@ -16,6 +16,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + ANSIBLE_METADATA = {'metadata_version': '1.0', 'status': ['preview'], 'supported_by': 'community'} From 487289644831c13e15ab880cb3eb510780979d7e Mon Sep 17 00:00:00 2001 From: fgbulsoni Date: Tue, 1 Aug 2017 15:54:16 -0300 Subject: [PATCH 13/19] Moved ResourceComparator methods to OneViewModuleBase and changed logger to AnsibleModule default .log method --- lib/ansible/module_utils/oneview.py | 391 ++---------------- .../hpe/oneview_fc_network.py | 1 - .../hpe/oneview_module_loader.py | 6 +- 3 files changed, 31 insertions(+), 367 deletions(-) diff --git a/lib/ansible/module_utils/oneview.py b/lib/ansible/module_utils/oneview.py index 6ba5257667e5bb..90e4a32d4ddbf9 100644 --- a/lib/ansible/module_utils/oneview.py +++ b/lib/ansible/module_utils/oneview.py @@ -234,7 +234,7 @@ def resource_present(self, resource, fact_name, create_method='create'): merged_data = resource.copy() merged_data.update(self.data) - if ResourceComparator.compare(resource, merged_data): + if self.compare(resource, merged_data): msg = self.MSG_ALREADY_PRESENT else: resource = self.resource_client.update(merged_data) @@ -247,12 +247,9 @@ def resource_present(self, resource, fact_name, create_method='create'): ansible_facts={fact_name: resource} ) - -class ResourceComparator(): MSG_DIFF_AT_KEY = 'Difference found at key \'{0}\'. ' - @staticmethod - def compare(first_resource, second_resource): + def compare(self, first_resource, second_resource): """ Recursively compares dictionary contents equivalence, ignoring types and elements order. Particularities of the comparison: @@ -263,15 +260,15 @@ def compare(first_resource, second_resource): :arg dict first_resource: first dictionary :arg dict second_resource: second dictionary :return: bool: True when equal, False when different. -""" + """ resource1 = deepcopy(first_resource) resource2 = deepcopy(second_resource) - # debug_resources = "resource1 = {0}, resource2 = {1}".format(resource1, resource2) + debug_resources = "resource1 = {0}, resource2 = {1}".format(resource1, resource2) # The first resource is True / Not Null and the second resource is False / Null if resource1 and not resource2: - # self.log("resource1 and not resource2. " + debug_resources) + self.module.log("resource1 and not resource2. " + debug_resources) return False # Checks all keys in first dict against the second dict @@ -279,24 +276,24 @@ def compare(first_resource, second_resource): if key not in resource2: if resource1[key] is not None: # Inexistent key is equivalent to exist with value None - # self.log(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) + self.module.log(self.MSG_DIFF_AT_KEY.format(key) + debug_resources) return False # If both values are null, empty or False it will be considered equal. elif not resource1[key] and not resource2[key]: continue elif isinstance(resource1[key], dict): # recursive call - if not ResourceComparator.compare(resource1[key], resource2[key]): - # self.log(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) + if not self.compare(resource1[key], resource2[key]): + self.module.log(self.MSG_DIFF_AT_KEY.format(key) + debug_resources) return False elif isinstance(resource1[key], list): # change comparison function to compare_list - if not ResourceComparator.compare_list(resource1[key], resource2[key]): - # self.log(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) + if not self.compare_list(resource1[key], resource2[key]): + self.module.log(self.MSG_DIFF_AT_KEY.format(key) + debug_resources) return False - elif ResourceComparator._standardize_value(resource1[key]) != ResourceComparator._standardize_value( + elif self._standardize_value(resource1[key]) != self._standardize_value( resource2[key]): - # self.log(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) + self.module.log(self.MSG_DIFF_AT_KEY.format(key) + debug_resources) return False # Checks all keys in the second dict, looking for missing elements @@ -304,13 +301,12 @@ def compare(first_resource, second_resource): if key not in resource1: if resource2[key] is not None: # Inexistent key is equivalent to exist with value None - # self.log(ResourceComparator.MSG_DIFF_AT_KEY.format(key) + debug_resources) + self.module.log(self.MSG_DIFF_AT_KEY.format(key) + debug_resources) return False return True - @staticmethod - def compare_list(first_resource, second_resource): + def compare_list(self, first_resource, second_resource): """ Recursively compares lists contents equivalence, ignoring types and element orders. Lists with same size are compared value by value after a sort, @@ -318,59 +314,57 @@ def compare_list(first_resource, second_resource): :arg list first_resource: first list :arg list second_resource: second list :return: True when equal; False when different. -""" + """ resource1 = deepcopy(first_resource) resource2 = deepcopy(second_resource) - # debug_resources = "resource1 = {0}, resource2 = {1}".format(resource1, resource2) + debug_resources = "resource1 = {0}, resource2 = {1}".format(resource1, resource2) # The second list is null / empty / False if not resource2: - # self.log("resource 2 is null. " + debug_resources) + self.module.log("resource 2 is null. " + debug_resources) return False if len(resource1) != len(resource2): - # self.log("resources have different length. " + debug_resources) + self.module.log("resources have different length. " + debug_resources) return False - resource1 = sorted(resource1, key=ResourceComparator._str_sorted) - resource2 = sorted(resource2, key=ResourceComparator._str_sorted) + resource1 = sorted(resource1, key=self._str_sorted) + resource2 = sorted(resource2, key=self._str_sorted) for i, val in enumerate(resource1): if isinstance(val, dict): # change comparison function to compare dictionaries - if not ResourceComparator.compare(val, resource2[i]): - # self.log("resources are different. " + debug_resources) + if not self.compare(val, resource2[i]): + self.module.log("resources are different. " + debug_resources) return False elif isinstance(val, list): # recursive call - if not ResourceComparator.compare_list(val, resource2[i]): - # self.log("lists are different. " + debug_resources) + if not self.compare_list(val, resource2[i]): + self.module.log("lists are different. " + debug_resources) return False - elif ResourceComparator._standardize_value(val) != ResourceComparator._standardize_value(resource2[i]): - # self.log("values are different. " + debug_resources) + elif self._standardize_value(val) != self._standardize_value(resource2[i]): + self.module.log("values are different. " + debug_resources) return False # no differences found return True - @staticmethod - def _str_sorted(obj): + def _str_sorted(self, obj): if isinstance(obj, dict): return json.dumps(obj, sort_keys=True) else: return str(obj) - @staticmethod - def _standardize_value(value): + def _standardize_value(self, value): """ Convert value to string to enhance the comparison. :arg value: Any object type. :return: str: Converted value. -""" + """ if isinstance(value, float) and value.is_integer(): # Workaround to avoid erroneous comparison between int and float # Removes zero from integer floats @@ -378,10 +372,7 @@ def _standardize_value(value): return str(value) - -class ResourceMerger(): - @staticmethod - def merge_list_by_key(original_list, updated_list, key, ignore_when_null=[]): + def merge_list_by_key(self, original_list, updated_list, key, ignore_when_null=[]): """ Merge two lists by the key. It basically: @@ -417,325 +408,3 @@ def merge_list_by_key(original_list, updated_list, key, ignore_when_null=[]): merged_items[item_key] = item.copy() return list(merged_items.values()) - # return [val for (_, val) in merged_items.items()] - - -class SPKeys(object): - ID = 'id' - NAME = 'name' - DEVICE_SLOT = 'deviceSlot' - CONNECTIONS = 'connections' - OS_DEPLOYMENT = 'osDeploymentSettings' - OS_DEPLOYMENT_URI = 'osDeploymentPlanUri' - ATTRIBUTES = 'osCustomAttributes' - SAN = 'sanStorage' - VOLUMES = 'volumeAttachments' - PATHS = 'storagePaths' - CONN_ID = 'connectionId' - BOOT = 'boot' - BIOS = 'bios' - BOOT_MODE = 'bootMode' - LOCAL_STORAGE = 'localStorage' - SAS_LOGICAL_JBODS = 'sasLogicalJBODs' - CONTROLLERS = 'controllers' - LOGICAL_DRIVES = 'logicalDrives' - SAS_LOGICAL_JBOD_URI = 'sasLogicalJBODUri' - SAS_LOGICAL_JBOD_ID = 'sasLogicalJBODId' - MODE = 'mode' - MAC_TYPE = 'macType' - MAC = 'mac' - SERIAL_NUMBER_TYPE = 'serialNumberType' - UUID = 'uuid' - SERIAL_NUMBER = 'serialNumber' - DRIVE_NUMBER = 'driveNumber' - WWPN_TYPE = 'wwpnType' - WWNN = 'wwnn' - WWPN = 'wwpn' - LUN_TYPE = 'lunType' - LUN = 'lun' - - -class ServerProfileMerger(object): - def merge_data(self, resource, data): - merged_data = deepcopy(resource) - merged_data.update(data) - - merged_data = self._merge_bios_and_boot(merged_data, resource, data) - merged_data = self._merge_connections(merged_data, resource, data) - merged_data = self._merge_san_storage(merged_data, data, resource) - merged_data = self._merge_os_deployment_settings(merged_data, resource, data) - merged_data = self._merge_local_storage(merged_data, resource, data) - - return merged_data - - def _merge_bios_and_boot(self, merged_data, resource, data): - if self._should_merge(data, resource, key=SPKeys.BIOS): - merged_data = self._merge_dict(merged_data, resource, data, key=SPKeys.BIOS) - if self._should_merge(data, resource, key=SPKeys.BOOT): - merged_data = self._merge_dict(merged_data, resource, data, key=SPKeys.BOOT) - if self._should_merge(data, resource, key=SPKeys.BOOT_MODE): - merged_data = self._merge_dict(merged_data, resource, data, key=SPKeys.BOOT_MODE) - return merged_data - - def _merge_connections(self, merged_data, resource, data): - if self._should_merge(data, resource, key=SPKeys.CONNECTIONS): - existing_connections = resource[SPKeys.CONNECTIONS] - params_connections = data[SPKeys.CONNECTIONS] - merged_data[SPKeys.CONNECTIONS] = ResourceMerger.merge_list_by_key(existing_connections, - params_connections, - key=SPKeys.ID) - - merged_data = self._merge_connections_boot(merged_data, resource) - return merged_data - - def _merge_connections_boot(self, merged_data, resource): - existing_connection_map = {} - for x in resource[SPKeys.CONNECTIONS]: - existing_connection_map[x[SPKeys.ID]] = x.copy() - for merged_connection in merged_data[SPKeys.CONNECTIONS]: - conn_id = merged_connection[SPKeys.ID] - existing_conn_has_boot = conn_id in existing_connection_map and SPKeys.BOOT in existing_connection_map[ - conn_id] - if existing_conn_has_boot and SPKeys.BOOT in merged_connection: - current_connection = existing_connection_map[conn_id] - boot_settings_merged = deepcopy(current_connection[SPKeys.BOOT]) - boot_settings_merged.update(merged_connection[SPKeys.BOOT]) - merged_connection[SPKeys.BOOT] = boot_settings_merged - return merged_data - - def _merge_san_storage(self, merged_data, data, resource): - if self._removed_data(data, resource, key=SPKeys.SAN): - merged_data[SPKeys.SAN] = dict(volumeAttachments=[], manageSanStorage=False) - elif self._should_merge(data, resource, key=SPKeys.SAN): - merged_data = self._merge_dict(merged_data, resource, data, key=SPKeys.SAN) - - merged_data = self._merge_san_volumes(merged_data, resource, data) - return merged_data - - def _merge_san_volumes(self, merged_data, resource, data): - if self._should_merge(data[SPKeys.SAN], resource[SPKeys.SAN], key=SPKeys.VOLUMES): - existing_volumes = resource[SPKeys.SAN][SPKeys.VOLUMES] - params_volumes = data[SPKeys.SAN][SPKeys.VOLUMES] - merged_volumes = ResourceMerger.merge_list_by_key(existing_volumes, params_volumes, key=SPKeys.ID) - merged_data[SPKeys.SAN][SPKeys.VOLUMES] = merged_volumes - - merged_data = self._merge_san_storage_paths(merged_data, resource) - return merged_data - - def _merge_san_storage_paths(self, merged_data, resource): - existing_volumes_map = collections.OrderedDict([(i[SPKeys.ID], i) for i in resource[ - SPKeys.SAN][SPKeys.VOLUMES]]) - merged_volumes = merged_data[SPKeys.SAN][SPKeys.VOLUMES] - for merged_volume in merged_volumes: - volume_id = merged_volume[SPKeys.ID] - if volume_id in existing_volumes_map: - if SPKeys.PATHS in merged_volume and SPKeys.PATHS in existing_volumes_map[volume_id]: - existent_paths = existing_volumes_map[volume_id][SPKeys.PATHS] - - paths_from_merged_volume = merged_volume[SPKeys.PATHS] - - merged_paths = ResourceMerger.merge_list_by_key(existent_paths, - paths_from_merged_volume, - key=SPKeys.CONN_ID) - - merged_volume[SPKeys.PATHS] = merged_paths - return merged_data - - def _merge_os_deployment_settings(self, merged_data, resource, data): - if self._should_merge(data, resource, key=SPKeys.OS_DEPLOYMENT): - merged_data = self._merge_dict(merged_data, resource, data, key=SPKeys.OS_DEPLOYMENT) - - merged_data = self._merge_os_deployment_custom_attr(merged_data, resource, data) - return merged_data - - def _merge_os_deployment_custom_attr(self, merged_data, resource, data): - if SPKeys.ATTRIBUTES in data[SPKeys.OS_DEPLOYMENT]: - existing_os_deployment = resource[SPKeys.OS_DEPLOYMENT] - params_os_deployment = data[SPKeys.OS_DEPLOYMENT] - merged_os_deployment = merged_data[SPKeys.OS_DEPLOYMENT] - - if self._removed_data(params_os_deployment, existing_os_deployment, key=SPKeys.ATTRIBUTES): - merged_os_deployment[SPKeys.ATTRIBUTES] = params_os_deployment[SPKeys.ATTRIBUTES] - else: - existing_attributes = existing_os_deployment[SPKeys.ATTRIBUTES] - params_attributes = params_os_deployment[SPKeys.ATTRIBUTES] - - if ResourceComparator.compare_list(existing_attributes, params_attributes): - merged_os_deployment[SPKeys.ATTRIBUTES] = existing_attributes - - return merged_data - - def _merge_local_storage(self, merged_data, resource, data): - if self._removed_data(data, resource, key=SPKeys.LOCAL_STORAGE): - merged_data[SPKeys.LOCAL_STORAGE] = dict(sasLogicalJBODs=[], controllers=[]) - elif self._should_merge(data, resource, key=SPKeys.LOCAL_STORAGE): - merged_data = self._merge_sas_logical_jbods(merged_data, resource, data) - merged_data = self._merge_controllers(merged_data, resource, data) - return merged_data - - def _merge_sas_logical_jbods(self, merged_data, resource, data): - if self._should_merge(data[SPKeys.LOCAL_STORAGE], resource[SPKeys.LOCAL_STORAGE], key=SPKeys.SAS_LOGICAL_JBODS): - existing_items = resource[SPKeys.LOCAL_STORAGE][SPKeys.SAS_LOGICAL_JBODS] - provided_items = merged_data[SPKeys.LOCAL_STORAGE][SPKeys.SAS_LOGICAL_JBODS] - merged_jbods = ResourceMerger.merge_list_by_key(existing_items, - provided_items, - key=SPKeys.ID, - ignore_when_null=[SPKeys.SAS_LOGICAL_JBOD_URI]) - merged_data[SPKeys.LOCAL_STORAGE][SPKeys.SAS_LOGICAL_JBODS] = merged_jbods - return merged_data - - def _merge_controllers(self, merged_data, resource, data): - if self._should_merge(data[SPKeys.LOCAL_STORAGE], resource[SPKeys.LOCAL_STORAGE], key=SPKeys.CONTROLLERS): - existing_items = resource[SPKeys.LOCAL_STORAGE][SPKeys.CONTROLLERS] - provided_items = merged_data[SPKeys.LOCAL_STORAGE][SPKeys.CONTROLLERS] - merged_controllers = ResourceMerger.merge_list_by_key(existing_items, - provided_items, - key=SPKeys.DEVICE_SLOT) - merged_data[SPKeys.LOCAL_STORAGE][SPKeys.CONTROLLERS] = merged_controllers - - merged_data = self._merge_controller_drives(merged_data, resource) - return merged_data - - def _merge_controller_drives(self, merged_data, resource): - for current_controller in merged_data[SPKeys.LOCAL_STORAGE][SPKeys.CONTROLLERS][:]: - for existing_controller in resource[SPKeys.LOCAL_STORAGE][SPKeys.CONTROLLERS][:]: - same_slot = current_controller.get(SPKeys.DEVICE_SLOT) == existing_controller.get(SPKeys.DEVICE_SLOT) - same_mode = existing_controller.get(SPKeys.MODE) == existing_controller.get(SPKeys.MODE) - if same_slot and same_mode and current_controller[SPKeys.LOGICAL_DRIVES]: - - key_merge = self._define_key_to_merge_drives(current_controller) - - if key_merge: - merged_drives = ResourceMerger.merge_list_by_key(existing_controller[SPKeys.LOGICAL_DRIVES], - current_controller[SPKeys.LOGICAL_DRIVES], - key=key_merge) - current_controller[SPKeys.LOGICAL_DRIVES] = merged_drives - return merged_data - - def _define_key_to_merge_drives(self, controller): - has_name = True - has_logical_jbod_id = True - for drive in controller[SPKeys.LOGICAL_DRIVES]: - if not drive.get(SPKeys.NAME): - has_name = False - if not drive.get(SPKeys.SAS_LOGICAL_JBOD_ID): - has_logical_jbod_id = False - - if has_name: - return SPKeys.NAME - elif has_logical_jbod_id: - return SPKeys.SAS_LOGICAL_JBOD_ID - return None - - def _removed_data(self, data, resource, key): - return key in data and not data[key] and key in resource - - def _should_merge(self, data, resource, key): - data_has_value = key in data and data[key] - existing_resource_has_value = key in resource and resource[key] - return data_has_value and existing_resource_has_value - - def _merge_dict(self, merged_data, resource, data, key): - if resource[key]: - merged_dict = deepcopy(resource[key]) - merged_dict.update(deepcopy(data[key])) - merged_data[key] = merged_dict - return merged_data - - -class ServerProfileReplaceNamesByUris(object): - SERVER_PROFILE_OS_DEPLOYMENT_NOT_FOUND = 'OS Deployment Plan not found: ' - SERVER_PROFILE_ENCLOSURE_GROUP_NOT_FOUND = 'Enclosure Group not found: ' - SERVER_PROFILE_NETWORK_NOT_FOUND = 'Network not found: ' - SERVER_HARDWARE_TYPE_NOT_FOUND = 'Server Hardware Type not found: ' - VOLUME_NOT_FOUND = 'Volume not found: ' - STORAGE_POOL_NOT_FOUND = 'Storage Pool not found: ' - STORAGE_SYSTEM_NOT_FOUND = 'Storage System not found: ' - INTERCONNECT_NOT_FOUND = 'Interconnect not found: ' - FIRMWARE_DRIVER_NOT_FOUND = 'Firmware Driver not found: ' - SAS_LOGICAL_JBOD_NOT_FOUND = 'SAS logical JBOD not found: ' - ENCLOSURE_NOT_FOUND = 'Enclosure not found: ' - - def replace(self, oneview_client, data): - self.oneview_client = oneview_client - self._replace_os_deployment_name_by_uri(data) - self._replace_enclosure_group_name_by_uri(data) - self._replace_networks_name_by_uri(data) - self._replace_server_hardware_type_name_by_uri(data) - self._replace_volume_attachment_names_by_uri(data) - self._replace_enclosure_name_by_uri(data) - self._replace_interconnect_name_by_uri(data) - self._replace_firmware_baseline_name_by_uri(data) - self._replace_sas_logical_jbod_name_by_uri(data) - - def _replace_name_by_uri(self, data, attr_name, message, resource_client): - attr_uri = attr_name.replace("Name", "Uri") - if attr_name in data: - name = data.pop(attr_name) - resource_by_name = resource_client.get_by('name', name) - if not resource_by_name: - raise HPOneViewResourceNotFound(message + name) - data[attr_uri] = resource_by_name[0]['uri'] - - def _replace_os_deployment_name_by_uri(self, data): - if SPKeys.OS_DEPLOYMENT in data and data[SPKeys.OS_DEPLOYMENT]: - self._replace_name_by_uri(data[SPKeys.OS_DEPLOYMENT], 'osDeploymentPlanName', - self.SERVER_PROFILE_OS_DEPLOYMENT_NOT_FOUND, - self.oneview_client.os_deployment_plans) - - def _replace_enclosure_group_name_by_uri(self, data): - self._replace_name_by_uri(data, 'enclosureGroupName', self.SERVER_PROFILE_ENCLOSURE_GROUP_NOT_FOUND, - self.oneview_client.enclosure_groups) - - def _replace_networks_name_by_uri(self, data): - if SPKeys.CONNECTIONS in data and data[SPKeys.CONNECTIONS]: - for connection in data[SPKeys.CONNECTIONS]: - if 'networkName' in connection: - name = connection.pop('networkName', None) - connection['networkUri'] = self._get_network_by_name(name)['uri'] - - def _replace_server_hardware_type_name_by_uri(self, data): - self._replace_name_by_uri(data, 'serverHardwareTypeName', self.SERVER_HARDWARE_TYPE_NOT_FOUND, - self.oneview_client.server_hardware_types) - - def _replace_volume_attachment_names_by_uri(self, data): - volume_attachments = (data.get('sanStorage') or {}).get('volumeAttachments') or [] - if len(volume_attachments) > 0: - for volume in volume_attachments: - self._replace_name_by_uri(volume, 'volumeName', self.VOLUME_NOT_FOUND, self.oneview_client.volumes) - self._replace_name_by_uri(volume, 'volumeStoragePoolName', self.STORAGE_POOL_NOT_FOUND, - self.oneview_client.storage_pools) - self._replace_name_by_uri(volume, 'volumeStorageSystemName', self.STORAGE_SYSTEM_NOT_FOUND, - self.oneview_client.storage_systems) - - def _replace_enclosure_name_by_uri(self, data): - self._replace_name_by_uri(data, 'enclosureName', self.ENCLOSURE_NOT_FOUND, self.oneview_client.enclosures) - - def _replace_interconnect_name_by_uri(self, data): - connections = data.get('connections') or [] - if len(connections) > 0: - for connection in connections: - self._replace_name_by_uri(connection, 'interconnectName', self.INTERCONNECT_NOT_FOUND, - self.oneview_client.interconnects) - - def _replace_firmware_baseline_name_by_uri(self, data): - firmware = data.get('firmware') or {} - self._replace_name_by_uri(firmware, 'firmwareBaselineName', self.FIRMWARE_DRIVER_NOT_FOUND, - self.oneview_client.firmware_drivers) - - def _replace_sas_logical_jbod_name_by_uri(self, data): - sas_logical_jbods = (data.get('localStorage') or {}).get('sasLogicalJBODs') or [] - if len(sas_logical_jbods) > 0: - for jbod in sas_logical_jbods: - self._replace_name_by_uri(jbod, 'sasLogicalJBODName', self.SAS_LOGICAL_JBOD_NOT_FOUND, - self.oneview_client.sas_logical_jbods) - - def _get_network_by_name(self, name): - fc_networks = self.oneview_client.fc_networks.get_by('name', name) - if fc_networks: - return fc_networks[0] - - ethernet_networks = self.oneview_client.ethernet_networks.get_by('name', name) - if not ethernet_networks: - raise HPOneViewResourceNotFound(self.SERVER_PROFILE_NETWORK_NOT_FOUND + name) - return ethernet_networks[0] diff --git a/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py b/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py index 2d51a3b3dc4720..6642631b035ee6 100644 --- a/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py +++ b/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py @@ -82,7 +82,6 @@ type: dict ''' -from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.oneview import OneViewModuleBase diff --git a/test/units/modules/remote_management/hpe/oneview_module_loader.py b/test/units/modules/remote_management/hpe/oneview_module_loader.py index 3a87eb8c34545b..dfbc0272878792 100644 --- a/test/units/modules/remote_management/hpe/oneview_module_loader.py +++ b/test/units/modules/remote_management/hpe/oneview_module_loader.py @@ -26,10 +26,6 @@ ONEVIEW_MODULE_UTILS_PATH = 'ansible.module_utils.oneview' from ansible.module_utils.oneview import (HPOneViewException, HPOneViewTaskError, - OneViewModuleBase, - SPKeys, - ServerProfileMerger, - ServerProfileReplaceNamesByUris, - ResourceComparator) + OneViewModuleBase) from ansible.modules.remote_management.hpe.oneview_fc_network import FcNetworkModule From a6cdde8bd0f8dc766146b866c00e5e0c096876db Mon Sep 17 00:00:00 2001 From: Felipe Bulsoni Date: Wed, 2 Aug 2017 12:07:20 -0300 Subject: [PATCH 14/19] Moved python requirement from module to doc fragments, removed deepcopies in comparissons, added set_scope action to fc_networks, made OneViewBaseModule an abc class --- lib/ansible/module_utils/oneview.py | 45 +++++++++++++++--- .../hpe/oneview_fc_network.py | 22 +++++++-- .../utils/module_docs_fragments/oneview.py | 3 ++ .../hpe/test_oneview_fc_network.py | 46 +++++++++++++++++++ 4 files changed, 106 insertions(+), 10 deletions(-) diff --git a/lib/ansible/module_utils/oneview.py b/lib/ansible/module_utils/oneview.py index 90e4a32d4ddbf9..8cd02a46997bf1 100644 --- a/lib/ansible/module_utils/oneview.py +++ b/lib/ansible/module_utils/oneview.py @@ -28,6 +28,7 @@ from __future__ import (absolute_import, division, print_function) +import abc import collections import json import logging @@ -65,7 +66,7 @@ def transform_list_to_dict(list_): return ret for value in list_: - if isinstance(value, dict): + if isinstance(value, collections.Mapping): ret.update(value) else: ret[to_native(value, errors='surrogate_or_strict')] = True @@ -74,6 +75,7 @@ def transform_list_to_dict(list_): class OneViewModuleBase(object): + __metaclass__ = abc.ABCMeta MSG_CREATED = 'Resource created successfully.' MSG_UPDATED = 'Resource updated successfully.' MSG_DELETED = 'Resource deleted successfully.' @@ -143,16 +145,17 @@ def _create_oneview_client(self): else: self.oneview_client = OneViewClient.from_json_file(self.module.params['config']) + @abc.abstractmethod def execute_module(self): """ - Abstract function, must be implemented by the inheritor. + Abstract method, must be implemented by the inheritor. This method is called from the run method. It should contains the module logic :return: dict: It must return a dictionary with the attributes for the module result, such as ansible_facts, msg and changed. """ - raise HPOneViewException("execute_module not implemented") + pass def run(self): """ @@ -249,6 +252,34 @@ def resource_present(self, resource, fact_name, create_method='create'): MSG_DIFF_AT_KEY = 'Difference found at key \'{0}\'. ' + def resource_scopes_set(self, state, fact_name, scope_uris): + """ + Generic implementation of the scopes update PATCH for the OneView resources. + It checks if the resource needs to be updated with the current scopes. + This method is meant to be run after ensuring the present state. + Args: + state (dict): + Dict containing the data from the last state results in the resource. + It needs to have the 'msg', 'changed', and 'ansible_facts' entries. + fact_name (str): + Name of the fact returned to the Ansible. + scope_uris (list) + List with all the scope URIs to be added to the resource. + Returns: + A dictionary with the expected arguments for the AnsibleModule.exit_json + """ + if scope_uris is None: + scope_uris = [] + resource = state['ansible_facts'][fact_name] + operation_data = dict(operation='replace', path='/scopeUris', value=scope_uris) + + if resource['scopeUris'] is None or set(resource['scopeUris']) != set(scope_uris): + state['ansible_facts'][fact_name] = self.resource_client.patch(resource['uri'], **operation_data) + state['changed'] = True + state['msg'] = self.MSG_UPDATED + + return state + def compare(self, first_resource, second_resource): """ Recursively compares dictionary contents equivalence, ignoring types and elements order. @@ -261,8 +292,8 @@ def compare(self, first_resource, second_resource): :arg dict second_resource: second dictionary :return: bool: True when equal, False when different. """ - resource1 = deepcopy(first_resource) - resource2 = deepcopy(second_resource) + resource1 = first_resource + resource2 = second_resource debug_resources = "resource1 = {0}, resource2 = {1}".format(resource1, resource2) @@ -316,8 +347,8 @@ def compare_list(self, first_resource, second_resource): :return: True when equal; False when different. """ - resource1 = deepcopy(first_resource) - resource2 = deepcopy(second_resource) + resource1 = first_resource + resource2 = second_resource debug_resources = "resource1 = {0}, resource2 = {1}".format(resource1, resource2) diff --git a/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py b/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py index 6642631b035ee6..c5786963a901d3 100644 --- a/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py +++ b/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py @@ -31,8 +31,7 @@ - Provides an interface to manage Fibre Channel Network resources. Can create, update, and delete. version_added: "2.4" requirements: - - "python >= 2.7.9" - - "hpOneView >= 3.1.0" + - "hpOneView >= 4.0.0" author: "Felipe Bulsoni (@fgbulsoni)" options: state: @@ -67,6 +66,16 @@ name: 'New FC Network' fabricType: 'DirectAttach' +- name: Ensure that the Fibre Channel Network is present and is inserted in the desired scopes + oneview_fc_network: + config: "{{ config_file_path }}" + state: present + data: + name: 'New FC Network' + scopeUris: + - '/rest/scopes/00SC123456' + - '/rest/scopes/01SC123456' + - name: Ensure that the Fibre Channel Network is absent oneview_fc_network: config: "{{ config_file_path }}" @@ -109,10 +118,17 @@ def execute_module(self): resource = self.get_by_name(self.data['name']) if self.state == 'present': - return self.resource_present(resource, self.RESOURCE_FACT_NAME) + return self.__present(resource) else: return self.resource_absent(resource) + def __present(self, resource): + scope_uris = self.data.pop('scopeUris', None) + result = self.resource_present(resource, self.RESOURCE_FACT_NAME) + if scope_uris is not None: + result = self.resource_scopes_set(result, 'fc_network', scope_uris) + return result + def main(): FcNetworkModule().run() diff --git a/lib/ansible/utils/module_docs_fragments/oneview.py b/lib/ansible/utils/module_docs_fragments/oneview.py index 03198ceb142f3c..f54319cb96d7f2 100644 --- a/lib/ansible/utils/module_docs_fragments/oneview.py +++ b/lib/ansible/utils/module_docs_fragments/oneview.py @@ -29,6 +29,9 @@ class ModuleDocFragment(object): For links to example configuration files or how to use the environment variables verify the notes section. required: false +requirements: + - "python >= 2.7.9" + notes: - "A sample configuration file for the config parameter can be found at: U(https://github.com/HewlettPackard/oneview-ansible/blob/master/examples/oneview_config-rename.json)" diff --git a/test/units/modules/remote_management/hpe/test_oneview_fc_network.py b/test/units/modules/remote_management/hpe/test_oneview_fc_network.py index 6249469a2414cb..39b8f8497af94c 100644 --- a/test/units/modules/remote_management/hpe/test_oneview_fc_network.py +++ b/test/units/modules/remote_management/hpe/test_oneview_fc_network.py @@ -126,3 +126,49 @@ def test_should_do_nothing_when_fc_network_not_exist(self): changed=False, msg=FcNetworkModule.MSG_ALREADY_ABSENT ) + + def test_update_scopes_when_different(self): + params_to_scope = PARAMS_FOR_PRESENT.copy() + params_to_scope['data']['scopeUris'] = ['test'] + self.mock_ansible_module.params = params_to_scope + + resource_data = DEFAULT_FC_NETWORK_TEMPLATE.copy() + resource_data['scopeUris'] = ['fake'] + resource_data['uri'] = 'rest/fc/fake' + self.resource.get_by.return_value = [resource_data] + + patch_return = resource_data.copy() + patch_return['scopeUris'] = ['test'] + self.resource.patch.return_value = patch_return + + FcNetworkModule().run() + + self.resource.patch.assert_called_once_with('rest/fc/fake', + operation='replace', + path='/scopeUris', + value=['test']) + + self.mock_ansible_module.exit_json.assert_called_once_with( + changed=True, + ansible_facts=dict(fc_network=patch_return), + msg=FcNetworkModule.MSG_UPDATED + ) + + def test_should_do_nothing_when_scopes_are_the_same(self): + params_to_scope = PARAMS_FOR_PRESENT.copy() + params_to_scope['data']['scopeUris'] = ['test'] + self.mock_ansible_module.params = params_to_scope + + resource_data = DEFAULT_FC_NETWORK_TEMPLATE.copy() + resource_data['scopeUris'] = ['test'] + self.resource.get_by.return_value = [resource_data] + + FcNetworkModule().run() + + self.resource.patch.not_been_called() + + self.mock_ansible_module.exit_json.assert_called_once_with( + changed=False, + ansible_facts=dict(fc_network=resource_data), + msg=FcNetworkModule.MSG_ALREADY_PRESENT + ) From a5e98bd46db1b2f1f55a55af5599fe218aef5d11 Mon Sep 17 00:00:00 2001 From: Felipe Bulsoni Date: Wed, 2 Aug 2017 13:51:07 -0300 Subject: [PATCH 15/19] Removed future required message since future is no longer used --- lib/ansible/module_utils/oneview.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/ansible/module_utils/oneview.py b/lib/ansible/module_utils/oneview.py index 8cd02a46997bf1..359088d1f05d8a 100644 --- a/lib/ansible/module_utils/oneview.py +++ b/lib/ansible/module_utils/oneview.py @@ -82,7 +82,6 @@ class OneViewModuleBase(object): MSG_ALREADY_PRESENT = 'Resource is already present.' MSG_ALREADY_ABSENT = 'Resource is already absent.' HPE_ONEVIEW_SDK_REQUIRED = 'HPE OneView Python SDK is required for this module.' - FUTURE_PACKAGE_REQUIRED = 'The Future Python package is required for this module.' ONEVIEW_COMMON_ARGS = dict( config=dict(required=False, type='str') From 80a76c958c1532edf49d031a295e33891f9e9ad9 Mon Sep 17 00:00:00 2001 From: Felipe Bulsoni Date: Wed, 2 Aug 2017 14:29:40 -0300 Subject: [PATCH 16/19] fixed imports order, removed logging import, changed style of add abcmeta class, removed extra underscores on methods and swapped isinstance validations of dict for collections.Mapping --- lib/ansible/module_utils/oneview.py | 18 ++++++++---------- .../hpe/oneview_fc_network.py | 4 ++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/ansible/module_utils/oneview.py b/lib/ansible/module_utils/oneview.py index 359088d1f05d8a..4e1378fbb72951 100644 --- a/lib/ansible/module_utils/oneview.py +++ b/lib/ansible/module_utils/oneview.py @@ -31,26 +31,24 @@ import abc import collections import json -import logging import os import traceback from copy import deepcopy -from ansible.module_utils import six -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native - try: from hpOneView.oneview_client import OneViewClient from hpOneView.exceptions import (HPOneViewException, HPOneViewTaskError, HPOneViewValueError, HPOneViewResourceNotFound) - HAS_HPE_ONEVIEW = True except ImportError: HAS_HPE_ONEVIEW = False +from ansible.module_utils import six +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native + def transform_list_to_dict(list_): """ @@ -74,8 +72,8 @@ def transform_list_to_dict(list_): return ret +@six.add_metaclass(abc.ABCMeta) class OneViewModuleBase(object): - __metaclass__ = abc.ABCMeta MSG_CREATED = 'Resource created successfully.' MSG_UPDATED = 'Resource updated successfully.' MSG_DELETED = 'Resource deleted successfully.' @@ -311,7 +309,7 @@ def compare(self, first_resource, second_resource): # If both values are null, empty or False it will be considered equal. elif not resource1[key] and not resource2[key]: continue - elif isinstance(resource1[key], dict): + elif isinstance(resource1[key], collections.Mapping): # recursive call if not self.compare(resource1[key], resource2[key]): self.module.log(self.MSG_DIFF_AT_KEY.format(key) + debug_resources) @@ -364,7 +362,7 @@ def compare_list(self, first_resource, second_resource): resource2 = sorted(resource2, key=self._str_sorted) for i, val in enumerate(resource1): - if isinstance(val, dict): + if isinstance(val, collections.Mapping): # change comparison function to compare dictionaries if not self.compare(val, resource2[i]): self.module.log("resources are different. " + debug_resources) @@ -382,7 +380,7 @@ def compare_list(self, first_resource, second_resource): return True def _str_sorted(self, obj): - if isinstance(obj, dict): + if isinstance(obj, collections.Mapping): return json.dumps(obj, sort_keys=True) else: return str(obj) diff --git a/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py b/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py index c5786963a901d3..c0d66b07b31f35 100644 --- a/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py +++ b/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py @@ -118,11 +118,11 @@ def execute_module(self): resource = self.get_by_name(self.data['name']) if self.state == 'present': - return self.__present(resource) + return self._present(resource) else: return self.resource_absent(resource) - def __present(self, resource): + def _present(self, resource): scope_uris = self.data.pop('scopeUris', None) result = self.resource_present(resource, self.RESOURCE_FACT_NAME) if scope_uris is not None: From c8d3fdc97dc21073b5d280247a1cf47b39bb1816 Mon Sep 17 00:00:00 2001 From: Felipe Bulsoni Date: Wed, 2 Aug 2017 15:36:41 -0300 Subject: [PATCH 17/19] Fixed docstrings and docstrings identation, removed a couple of copy methods from merge_lists and moved MSG that was in the middle of the code to top of the class --- lib/ansible/module_utils/oneview.py | 47 ++++++++++++----------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/lib/ansible/module_utils/oneview.py b/lib/ansible/module_utils/oneview.py index 4e1378fbb72951..abdb4a40b5f681 100644 --- a/lib/ansible/module_utils/oneview.py +++ b/lib/ansible/module_utils/oneview.py @@ -33,7 +33,6 @@ import json import os import traceback -from copy import deepcopy try: from hpOneView.oneview_client import OneViewClient @@ -56,7 +55,7 @@ def transform_list_to_dict(list_): :arg list list_: List of values :return: dict: dictionary built -""" + """ ret = {} @@ -79,6 +78,7 @@ class OneViewModuleBase(object): MSG_DELETED = 'Resource deleted successfully.' MSG_ALREADY_PRESENT = 'Resource is already present.' MSG_ALREADY_ABSENT = 'Resource is already absent.' + MSG_DIFF_AT_KEY = 'Difference found at key \'{0}\'. ' HPE_ONEVIEW_SDK_REQUIRED = 'HPE OneView Python SDK is required for this module.' ONEVIEW_COMMON_ARGS = dict( @@ -100,7 +100,7 @@ def __init__(self, additional_arg_spec=None, validate_etag_support=False): :arg dict additional_arg_spec: Additional argument spec definition. :arg bool validate_etag_support: Enables support to eTag validation. -""" + """ argument_spec = self._build_argument_spec(additional_arg_spec, validate_etag_support) self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) @@ -151,7 +151,7 @@ def execute_module(self): :return: dict: It must return a dictionary with the attributes for the module result, such as ansible_facts, msg and changed. -""" + """ pass def run(self): @@ -162,7 +162,7 @@ def run(self): It handles any HPOneViewException in order to signal a failure to Ansible, with a descriptive error message. -""" + """ try: if self.validate_etag_support: if not self.module.params.get('validate_etag'): @@ -189,7 +189,7 @@ def resource_absent(self, resource, method='delete'): :arg str method: Function of the OneView client that will be called for resource deletion. Usually delete or remove. :return: A dictionary with the expected arguments for the AnsibleModule.exit_json -""" + """ if resource: getattr(self.resource_client, method)(resource) @@ -204,7 +204,7 @@ def get_by_name(self, name): :arg str name: Resource name to search for. :return: The resource found or None. -""" + """ result = self.resource_client.get_by('name', name) return result[0] if result else None @@ -219,7 +219,7 @@ def resource_present(self, resource, fact_name, create_method='create'): :arg str create_method: Function of the OneView client that will be called for resource creation. Usually create or add. :return: A dictionary with the expected arguments for the AnsibleModule.exit_json -""" + """ changed = False if "newName" in self.data: @@ -247,24 +247,17 @@ def resource_present(self, resource, fact_name, create_method='create'): ansible_facts={fact_name: resource} ) - MSG_DIFF_AT_KEY = 'Difference found at key \'{0}\'. ' - def resource_scopes_set(self, state, fact_name, scope_uris): """ Generic implementation of the scopes update PATCH for the OneView resources. It checks if the resource needs to be updated with the current scopes. This method is meant to be run after ensuring the present state. - Args: - state (dict): - Dict containing the data from the last state results in the resource. - It needs to have the 'msg', 'changed', and 'ansible_facts' entries. - fact_name (str): - Name of the fact returned to the Ansible. - scope_uris (list) - List with all the scope URIs to be added to the resource. - Returns: - A dictionary with the expected arguments for the AnsibleModule.exit_json - """ + :arg dict state: Dict containing the data from the last state results in the resource. + It needs to have the 'msg', 'changed', and 'ansible_facts' entries. + :arg str fact_name: Name of the fact returned to the Ansible. + :arg list scope_uris: List with all the scope URIs to be added to the resource. + :return: A dictionary with the expected arguments for the AnsibleModule.exit_json + """ if scope_uris is None: scope_uris = [] resource = state['ansible_facts'][fact_name] @@ -288,7 +281,7 @@ def compare(self, first_resource, second_resource): :arg dict first_resource: first dictionary :arg dict second_resource: second dictionary :return: bool: True when equal, False when different. - """ + """ resource1 = first_resource resource2 = second_resource @@ -342,7 +335,7 @@ def compare_list(self, first_resource, second_resource): :arg list first_resource: first list :arg list second_resource: second list :return: True when equal; False when different. - """ + """ resource1 = first_resource resource2 = second_resource @@ -392,7 +385,7 @@ def _standardize_value(self, value): :arg value: Any object type. :return: str: Converted value. - """ + """ if isinstance(value, float) and value.is_integer(): # Workaround to avoid erroneous comparison between int and float # Removes zero from integer floats @@ -416,7 +409,7 @@ def merge_list_by_key(self, original_list, updated_list, key, ignore_when_null=[ :arg list ignore_when_null: list with the keys from the updated items that should be ignored in the merge, if its values are null. :return: list: Lists merged. -""" + """ if not original_list: return updated_list @@ -430,9 +423,9 @@ def merge_list_by_key(self, original_list, updated_list, key, ignore_when_null=[ for ignored_key in ignore_when_null: if ignored_key in item and not item[ignored_key]: item.pop(ignored_key) - merged_items[item_key] = items_map[item_key].copy() + merged_items[item_key] = items_map[item_key] merged_items[item_key].update(item) else: - merged_items[item_key] = item.copy() + merged_items[item_key] = item return list(merged_items.values()) From e36e64b4c2a75e88850a4f7230358e3f0e3a4dbd Mon Sep 17 00:00:00 2001 From: Felipe Bulsoni Date: Wed, 2 Aug 2017 16:57:36 -0300 Subject: [PATCH 18/19] Changed condition of not to is None according to review. --- lib/ansible/module_utils/oneview.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ansible/module_utils/oneview.py b/lib/ansible/module_utils/oneview.py index abdb4a40b5f681..285537ff1b6255 100644 --- a/lib/ansible/module_utils/oneview.py +++ b/lib/ansible/module_utils/oneview.py @@ -293,7 +293,7 @@ def compare(self, first_resource, second_resource): return False # Checks all keys in first dict against the second dict - for key in resource1.keys(): + for key in resource1: if key not in resource2: if resource1[key] is not None: # Inexistent key is equivalent to exist with value None @@ -421,7 +421,7 @@ def merge_list_by_key(self, original_list, updated_list, key, ignore_when_null=[ item_key = item[key] if item_key in items_map: for ignored_key in ignore_when_null: - if ignored_key in item and not item[ignored_key]: + if ignored_key in item and item[ignored_key] is None: item.pop(ignored_key) merged_items[item_key] = items_map[item_key] merged_items[item_key].update(item) From 09142f4f1496704b830b68998412b7170e3f3cbc Mon Sep 17 00:00:00 2001 From: Felipe Bulsoni Date: Wed, 2 Aug 2017 17:59:37 -0300 Subject: [PATCH 19/19] Moving toplevel functions out of OneViewBaseClass and changing the license header in oneview_fc_network.py to short version --- lib/ansible/module_utils/oneview.py | 128 +++++++++--------- .../hpe/oneview_fc_network.py | 18 +-- 2 files changed, 67 insertions(+), 79 deletions(-) diff --git a/lib/ansible/module_utils/oneview.py b/lib/ansible/module_utils/oneview.py index 285537ff1b6255..49769d5c2451fc 100644 --- a/lib/ansible/module_utils/oneview.py +++ b/lib/ansible/module_utils/oneview.py @@ -71,6 +71,67 @@ def transform_list_to_dict(list_): return ret +def merge_list_by_key(original_list, updated_list, key, ignore_when_null=[]): + """ + Merge two lists by the key. It basically: + + 1. Adds the items that are present on updated_list and are absent on original_list. + + 2. Removes items that are absent on updated_list and are present on original_list. + + 3. For all items that are in both lists, overwrites the values from the original item by the updated item. + + :arg list original_list: original list. + :arg list updated_list: list with changes. + :arg str key: unique identifier. + :arg list ignore_when_null: list with the keys from the updated items that should be ignored in the merge, + if its values are null. + :return: list: Lists merged. + """ + if not original_list: + return updated_list + + items_map = collections.OrderedDict([(i[key], i.copy()) for i in original_list]) + + merged_items = collections.OrderedDict() + + for item in updated_list: + item_key = item[key] + if item_key in items_map: + for ignored_key in ignore_when_null: + if ignored_key in item and item[ignored_key] is None: + item.pop(ignored_key) + merged_items[item_key] = items_map[item_key] + merged_items[item_key].update(item) + else: + merged_items[item_key] = item + + return list(merged_items.values()) + + +def _str_sorted(obj): + if isinstance(obj, collections.Mapping): + return json.dumps(obj, sort_keys=True) + else: + return str(obj) + + +def _standardize_value(value): + """ + Convert value to string to enhance the comparison. + + :arg value: Any object type. + + :return: str: Converted value. + """ + if isinstance(value, float) and value.is_integer(): + # Workaround to avoid erroneous comparison between int and float + # Removes zero from integer floats + value = int(value) + + return str(value) + + @six.add_metaclass(abc.ABCMeta) class OneViewModuleBase(object): MSG_CREATED = 'Resource created successfully.' @@ -312,8 +373,7 @@ def compare(self, first_resource, second_resource): if not self.compare_list(resource1[key], resource2[key]): self.module.log(self.MSG_DIFF_AT_KEY.format(key) + debug_resources) return False - elif self._standardize_value(resource1[key]) != self._standardize_value( - resource2[key]): + elif _standardize_value(resource1[key]) != _standardize_value(resource2[key]): self.module.log(self.MSG_DIFF_AT_KEY.format(key) + debug_resources) return False @@ -351,8 +411,8 @@ def compare_list(self, first_resource, second_resource): self.module.log("resources have different length. " + debug_resources) return False - resource1 = sorted(resource1, key=self._str_sorted) - resource2 = sorted(resource2, key=self._str_sorted) + resource1 = sorted(resource1, key=_str_sorted) + resource2 = sorted(resource2, key=_str_sorted) for i, val in enumerate(resource1): if isinstance(val, collections.Mapping): @@ -365,67 +425,9 @@ def compare_list(self, first_resource, second_resource): if not self.compare_list(val, resource2[i]): self.module.log("lists are different. " + debug_resources) return False - elif self._standardize_value(val) != self._standardize_value(resource2[i]): + elif _standardize_value(val) != _standardize_value(resource2[i]): self.module.log("values are different. " + debug_resources) return False # no differences found return True - - def _str_sorted(self, obj): - if isinstance(obj, collections.Mapping): - return json.dumps(obj, sort_keys=True) - else: - return str(obj) - - def _standardize_value(self, value): - """ - Convert value to string to enhance the comparison. - - :arg value: Any object type. - - :return: str: Converted value. - """ - if isinstance(value, float) and value.is_integer(): - # Workaround to avoid erroneous comparison between int and float - # Removes zero from integer floats - value = int(value) - - return str(value) - - def merge_list_by_key(self, original_list, updated_list, key, ignore_when_null=[]): - """ - Merge two lists by the key. It basically: - - 1. Adds the items that are present on updated_list and are absent on original_list. - - 2. Removes items that are absent on updated_list and are present on original_list. - - 3. For all items that are in both lists, overwrites the values from the original item by the updated item. - - :arg list original_list: original list. - :arg list updated_list: list with changes. - :arg str key: unique identifier. - :arg list ignore_when_null: list with the keys from the updated items that should be ignored in the merge, - if its values are null. - :return: list: Lists merged. - """ - if not original_list: - return updated_list - - items_map = collections.OrderedDict([(i[key], i.copy()) for i in original_list]) - - merged_items = collections.OrderedDict() - - for item in updated_list: - item_key = item[key] - if item_key in items_map: - for ignored_key in ignore_when_null: - if ignored_key in item and item[ignored_key] is None: - item.pop(ignored_key) - merged_items[item_key] = items_map[item_key] - merged_items[item_key].update(item) - else: - merged_items[item_key] = item - - return list(merged_items.values()) diff --git a/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py b/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py index c0d66b07b31f35..602f7b286014c7 100644 --- a/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py +++ b/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py @@ -1,20 +1,6 @@ #!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright (2016-2017) Hewlett Packard Enterprise Development LP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# Copyright (c) 2016-2017 Hewlett Packard Enterprise Development LP +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import (absolute_import, division, print_function) __metaclass__ = type