diff --git a/lib/ansible/module_utils/oneview.py b/lib/ansible/module_utils/oneview.py
new file mode 100644
index 0000000000..49769d5c24
--- /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 0000000000..e69de29bb2
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 0000000000..602f7b2860
--- /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 0000000000..f54319cb96
--- /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 0000000000..e69de29bb2
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 0000000000..4e2d53a13d
--- /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 0000000000..dfbc027287
--- /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 0000000000..39b8f8497a
--- /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
+ )