mirror of https://github.com/ansible/ansible.git
Initial commits for integration of HPE OneView resources with Ansible (#26026)
* 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.pull/26272/head
parent
8bb10bb225
commit
b060d0ccba
@ -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
|
@ -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()
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
'''
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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()
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
Loading…
Reference in New Issue