diff --git a/lib/ansible/module_utils/oneview.py b/lib/ansible/module_utils/oneview.py new file mode 100644 index 00000000000..49769d5c245 --- /dev/null +++ b/lib/ansible/module_utils/oneview.py @@ -0,0 +1,433 @@ +# 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. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * 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. +# +# 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) + +import abc +import collections +import json +import os +import traceback + +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_): + """ + 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, collections.Mapping): + ret.update(value) + else: + ret[to_native(value, errors='surrogate_or_strict')] = True + + 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.' + MSG_UPDATED = 'Resource updated successfully.' + 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( + 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. + + :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.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 = 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']) + + @abc.abstractmethod + def execute_module(self): + """ + 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. + """ + pass + + 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: + 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. + + :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) + + 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. + + :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. + + :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: + 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 self.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} + ) + + 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. + :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] + 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. + 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. + :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 + + 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.module.log("resource1 and not resource2. " + debug_resources) + return False + + # Checks all keys in first dict against the second dict + for key in resource1: + if key not in resource2: + if resource1[key] is not None: + # Inexistent key is equivalent to exist with value None + 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], collections.Mapping): + # recursive call + 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 self.compare_list(resource1[key], resource2[key]): + self.module.log(self.MSG_DIFF_AT_KEY.format(key) + debug_resources) + return False + elif _standardize_value(resource1[key]) != _standardize_value(resource2[key]): + 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 + 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 + self.module.log(self.MSG_DIFF_AT_KEY.format(key) + debug_resources) + return False + + return True + + 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, + each element is converted to str before the comparison. + :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 + + debug_resources = "resource1 = {0}, resource2 = {1}".format(resource1, resource2) + + # The second list is null / empty / False + if not resource2: + self.module.log("resource 2 is null. " + debug_resources) + return False + + if len(resource1) != len(resource2): + self.module.log("resources have different length. " + debug_resources) + return False + + resource1 = sorted(resource1, key=_str_sorted) + resource2 = sorted(resource2, key=_str_sorted) + + for i, val in enumerate(resource1): + 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) + return False + elif isinstance(val, list): + # recursive call + if not self.compare_list(val, resource2[i]): + self.module.log("lists are different. " + debug_resources) + return False + elif _standardize_value(val) != _standardize_value(resource2[i]): + self.module.log("values are different. " + debug_resources) + return False + + # no differences found + return True diff --git a/lib/ansible/modules/remote_management/hpe/__init__.py b/lib/ansible/modules/remote_management/hpe/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py b/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py new file mode 100644 index 00000000000..602f7b28601 --- /dev/null +++ b/lib/ansible/modules/remote_management/hpe/oneview_fc_network.py @@ -0,0 +1,124 @@ +#!/usr/bin/python +# 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 + +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community'} + +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: + - "hpOneView >= 4.0.0" +author: "Felipe Bulsoni (@fgbulsoni)" +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 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 }}" + 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: dict +''' + +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._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() + + +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 00000000000..f54319cb96d --- /dev/null +++ b/lib/ansible/utils/module_docs_fragments/oneview.py @@ -0,0 +1,65 @@ +# -*- 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 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 + +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)" + - "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/units/modules/remote_management/__init__.py b/test/units/modules/remote_management/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/units/modules/remote_management/hpe/hpe_test_utils.py b/test/units/modules/remote_management/hpe/hpe_test_utils.py new file mode 100644 index 00000000000..4e2d53a13de --- /dev/null +++ b/test/units/modules/remote_management/hpe/hpe_test_utils.py @@ -0,0 +1,137 @@ +# -*- 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 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): + # 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.remote_management.hpe, testing_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/remote_management/hpe/oneview_module_loader.py b/test/units/modules/remote_management/hpe/oneview_module_loader.py new file mode 100644 index 00000000000..dfbc0272878 --- /dev/null +++ b/test/units/modules/remote_management/hpe/oneview_module_loader.py @@ -0,0 +1,31 @@ +# -*- 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 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() + +ONEVIEW_MODULE_UTILS_PATH = 'ansible.module_utils.oneview' +from ansible.module_utils.oneview import (HPOneViewException, + HPOneViewTaskError, + OneViewModuleBase) + +from ansible.modules.remote_management.hpe.oneview_fc_network import FcNetworkModule 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 new file mode 100644 index 00000000000..39b8f8497af --- /dev/null +++ b/test/units/modules/remote_management/hpe/test_oneview_fc_network.py @@ -0,0 +1,174 @@ +# -*- 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 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 + ) + + 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 + )