mirror of https://github.com/ansible/ansible.git
Resolve issues in NetApp E-Series Host module (#39748)
* Resolve issues in NetApp E-Series Host module The E-Series host module had some bugs relating to the update/creation of host definitions when iSCSI initiators when included in the configuration. This patch resolves this and other minor issues with correctly detecting updates. There were also several minor issues found that were causing issues with truly idepotent updates/changes to the host definition. This patch also provides some unit tests and integration tests to help catch future issues in these areas. fixes #28272 * Improve NetApp E-Series Host module testing The NetApp E-Series Host module integration test lacked feature test verification to verify the changes made to the storage array. The NetApp E-Series rest api was used to verify host create, update, and remove changes made to the NetApp E-Series storage arrays.pull/44643/head
parent
3122860f22
commit
ad91793428
@ -0,0 +1,10 @@
|
||||
# This test is not enabled by default, but can be utilized by defining required variables in integration_config.yml
|
||||
# Example integration_config.yml:
|
||||
# ---
|
||||
#netapp_e_api_host: 10.113.1.111:8443
|
||||
#netapp_e_api_username: admin
|
||||
#netapp_e_api_password: myPass
|
||||
#netapp_e_ssid: 1
|
||||
|
||||
unsupported
|
||||
netapp/eseries
|
@ -0,0 +1 @@
|
||||
- include_tasks: run.yml
|
@ -0,0 +1,276 @@
|
||||
---
|
||||
# Test code for the netapp_e_host module
|
||||
# (c) 2018, NetApp, Inc
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
- name: NetApp Test Host module
|
||||
fail:
|
||||
msg: 'Please define netapp_e_api_username, netapp_e_api_password, netapp_e_api_host, and netapp_e_ssid.'
|
||||
when: netapp_e_api_username is undefined or netapp_e_api_password is undefined or
|
||||
netapp_e_api_host is undefined or netapp_e_ssid is undefined
|
||||
vars:
|
||||
gather_facts: yes
|
||||
credentials: &creds
|
||||
api_url: "https://{{ netapp_e_api_host }}/devmgr/v2"
|
||||
api_username: "{{ netapp_e_api_username }}"
|
||||
api_password: "{{ netapp_e_api_password }}"
|
||||
ssid: "{{ netapp_e_ssid }}"
|
||||
validate_certs: no
|
||||
hosts: &hosts
|
||||
1:
|
||||
host_type: 27
|
||||
update_host_type: 28
|
||||
ports:
|
||||
- type: 'iscsi'
|
||||
label: 'I_1'
|
||||
port: 'iqn.1996-04.de.suse:01:56f86f9bd1fe'
|
||||
- type: 'iscsi'
|
||||
label: 'I_2'
|
||||
port: 'iqn.1996-04.de.suse:01:56f86f9bd1ff'
|
||||
ports2:
|
||||
- type: 'iscsi'
|
||||
label: 'I_1'
|
||||
port: 'iqn.1996-04.de.suse:01:56f86f9bd1fe'
|
||||
- type: 'iscsi'
|
||||
label: 'I_2'
|
||||
port: 'iqn.1996-04.de.suse:01:56f86f9bd1ff'
|
||||
- type: 'fc'
|
||||
label: 'FC_3'
|
||||
port: '10:00:8C:7C:FF:1A:B9:01'
|
||||
2:
|
||||
host_type: 27
|
||||
update_host_type: 28
|
||||
ports:
|
||||
- type: 'fc'
|
||||
label: 'FC_1'
|
||||
port: '10:00:8C:7C:FF:1A:B9:01'
|
||||
- type: 'fc'
|
||||
label: 'FC_2'
|
||||
port: '10:00:8C:7C:FF:1A:B9:00'
|
||||
ports2:
|
||||
- type: 'fc'
|
||||
label: 'FC_6'
|
||||
port: '10:00:8C:7C:FF:1A:B9:01'
|
||||
- type: 'fc'
|
||||
label: 'FC_4'
|
||||
port: '10:00:8C:7C:FF:1A:B9:00'
|
||||
|
||||
|
||||
# ********************************************
|
||||
# *** Ensure jmespath package is installed ***
|
||||
# ********************************************
|
||||
# NOTE: jmespath must be installed for the json_query filter
|
||||
- name: Ensure that jmespath is installed
|
||||
pip:
|
||||
name: jmespath
|
||||
state: present
|
||||
register: jmespath
|
||||
- fail:
|
||||
msg: "Restart playbook, the jmespath package was installed and is need for the playbook's execution."
|
||||
when: jmespath.changed
|
||||
|
||||
|
||||
# *****************************************
|
||||
# *** Set credential and host variables ***
|
||||
# *****************************************
|
||||
- name: Set hosts variable
|
||||
set_fact:
|
||||
hosts: *hosts
|
||||
- name: set credentials
|
||||
set_fact:
|
||||
credentials: *creds
|
||||
- name: Show some debug information
|
||||
debug:
|
||||
msg: "Using user={{ credentials.api_username }} on server={{ credentials.api_url }}."
|
||||
|
||||
# *** Remove any existing hosts to set initial state and verify state ***
|
||||
- name: Remove any existing hosts
|
||||
netapp_e_host:
|
||||
<<: *creds
|
||||
state: absent
|
||||
name: "{{ item.key }}"
|
||||
with_dict: *hosts
|
||||
|
||||
# Retrieve array host definitions
|
||||
- name: HTTP request for all host definitions from array
|
||||
uri:
|
||||
url: "{{ credentials.api_url }}/storage-systems/{{ credentials.ssid }}/hosts"
|
||||
user: "{{ credentials.api_username }}"
|
||||
password: "{{ credentials.api_password }}"
|
||||
body_format: json
|
||||
validate_certs: no
|
||||
register: result
|
||||
|
||||
# Verify that host 1 and 2 host objects do not exist
|
||||
- name: Collect host side port labels
|
||||
set_fact:
|
||||
host_labels: "{{ result | json_query('json[*].label') }}"
|
||||
- name: Assert hosts were removed
|
||||
assert:
|
||||
that: "'{{ item.key }}' not in host_labels"
|
||||
msg: "Host, {{ item.key }}, failed to be removed from the hosts!"
|
||||
loop: "{{ lookup('dict', hosts) }}"
|
||||
|
||||
|
||||
# *****************************************************************
|
||||
# *** Create host definitions and validate host object creation ***
|
||||
# *****************************************************************
|
||||
- name: Define hosts
|
||||
netapp_e_host:
|
||||
<<: *creds
|
||||
state: present
|
||||
host_type: "{{ item.value.host_type }}"
|
||||
ports: "{{ item.value.ports }}"
|
||||
name: "{{ item.key }}"
|
||||
with_dict: *hosts
|
||||
|
||||
# Retrieve array host definitions
|
||||
- name: https request to validate host definitions were created
|
||||
uri:
|
||||
url: "{{ credentials.api_url }}/storage-systems/{{ credentials.ssid }}/hosts"
|
||||
user: "{{ credentials.api_username }}"
|
||||
password: "{{ credentials.api_password }}"
|
||||
body_format: json
|
||||
validate_certs: no
|
||||
register: result
|
||||
|
||||
# Verify hosts were indeed created
|
||||
- name: Collect host label list
|
||||
set_fact:
|
||||
hosts_labels: "{{ result | json_query('json[*].label') }}"
|
||||
- name: Validate hosts were in fact created
|
||||
assert:
|
||||
that: "'{{ item.key }}' in hosts_labels"
|
||||
msg: "host, {{ item.key }}, not define on array!"
|
||||
loop: "{{ lookup('dict', hosts) }}"
|
||||
|
||||
# *** Update with no state changes results in no changes ***
|
||||
- name: Redefine hosts, expecting no changes
|
||||
netapp_e_host:
|
||||
<<: *creds
|
||||
state: present
|
||||
host_type: "{{ item.value.host_type }}"
|
||||
ports: "{{ item.value.ports }}"
|
||||
name: "{{ item.key }}"
|
||||
with_dict: *hosts
|
||||
register: result
|
||||
|
||||
# Verify that no changes occurred
|
||||
- name: Ensure no change occurred
|
||||
assert:
|
||||
msg: "A change was not detected!"
|
||||
that: "not result.changed"
|
||||
|
||||
|
||||
# ***********************************************************************************
|
||||
# *** Redefine hosts using ports2 host definitions and validate the updated state ***
|
||||
# ***********************************************************************************
|
||||
- name: Redefine hosts, expecting changes
|
||||
netapp_e_host:
|
||||
<<: *creds
|
||||
state: present
|
||||
host_type: "{{ item.value.host_type }}"
|
||||
ports: "{{ item.value.ports2 }}"
|
||||
name: "{{ item.key }}"
|
||||
force_port: yes
|
||||
with_dict: *hosts
|
||||
register: result
|
||||
|
||||
# Request from the array all host definitions
|
||||
- name: HTTP request for port information
|
||||
uri:
|
||||
url: "{{ credentials.api_url }}/storage-systems/{{ credentials.ssid }}/hosts"
|
||||
user: "{{ credentials.api_username }}"
|
||||
password: "{{ credentials.api_password }}"
|
||||
body_format: json
|
||||
validate_certs: no
|
||||
register: result
|
||||
|
||||
# Compile a list of array host port information for verifying changes
|
||||
- name: Compile array host port information list
|
||||
set_fact:
|
||||
tmp: []
|
||||
|
||||
# Append each loop to the previous extraction. Each loop consists of host definitions and the filters will perform
|
||||
# the following: grab host side port lists; combine to each list a dictionary containing the host name(label);
|
||||
# lastly, convert the zip_longest object into a list
|
||||
- set_fact:
|
||||
tmp: "{{ tmp }} + {{ [item | json_query('hostSidePorts[*]')] |
|
||||
zip_longest([], fillvalue={'host_name': item.label}) | list }}"
|
||||
loop: "{{ result.json }}"
|
||||
|
||||
# Make new list, port_info, by combining each list entry's dictionaries into a single dictionary
|
||||
- name: Create port information list
|
||||
set_fact:
|
||||
port_info: []
|
||||
- set_fact:
|
||||
port_info: "{{ port_info }} + [{{ item[0] |combine(item[1]) }}]"
|
||||
loop: "{{ tmp }}"
|
||||
|
||||
# Compile list of expected host port information for verifying changes
|
||||
- name: Create expected port information list
|
||||
set_fact:
|
||||
tmp: []
|
||||
|
||||
# Append each loop to the previous extraction. Each loop consists of host definitions and the filters will perform
|
||||
# the following: grab host side port lists; combine to each list a dictionary containing the host name(label);
|
||||
# lastly, convert the zip_longest object into a list
|
||||
- set_fact:
|
||||
tmp: "{{ tmp }} + {{ [item | json_query('value.ports2[*]')]|
|
||||
zip_longest([], fillvalue={'host_name': item.key|string}) | list }}"
|
||||
loop: "{{ lookup('dict', hosts) }}"
|
||||
|
||||
# Make new list, expected_port_info, by combining each list entry's dictionaries into a single dictionary
|
||||
- name: Create expected port information list
|
||||
set_fact:
|
||||
expected_port_info: []
|
||||
- set_fact:
|
||||
expected_port_info: "{{ expected_port_info }} + [{{ item[0] |combine(item[1]) }}]"
|
||||
loop: "{{ tmp }}"
|
||||
|
||||
# Verify that each host object has the expected protocol type and address/port
|
||||
- name: Assert hosts information was updated with new port information
|
||||
assert:
|
||||
that: "{{ item[0].host_name != item[1].host_name or
|
||||
item[0].label != item[1].label or
|
||||
(item[0].type == item[1].type and
|
||||
(item[0].address|regex_replace(':','')) == (item[1].port|regex_replace(':',''))) }}"
|
||||
msg: "port failed to be updated!"
|
||||
loop: "{{ query('nested', port_info, expected_port_info) }}"
|
||||
|
||||
|
||||
# ****************************************************
|
||||
# *** Remove any existing hosts and verify changes ***
|
||||
# ****************************************************
|
||||
- name: Remove any existing hosts
|
||||
netapp_e_host:
|
||||
<<: *creds
|
||||
state: absent
|
||||
name: "{{ item.key }}"
|
||||
with_dict: *hosts
|
||||
|
||||
# Request all host object definitions
|
||||
- name: HTTP request for all host definitions from array
|
||||
uri:
|
||||
url: "{{ credentials.api_url }}/storage-systems/{{ credentials.ssid }}/hosts"
|
||||
user: "{{ credentials.api_username }}"
|
||||
password: "{{ credentials.api_password }}"
|
||||
body_format: json
|
||||
validate_certs: no
|
||||
register: results
|
||||
|
||||
# Collect port label information
|
||||
- name: Collect host side port labels
|
||||
set_fact:
|
||||
host_side_port_labels: "{{ results | json_query('json[*].hostSidePorts[*].label') }}"
|
||||
|
||||
- name: Collect removed port labels
|
||||
set_fact:
|
||||
removed_host_side_port_labels: "{{ hosts | json_query('*.ports[*].label') }}"
|
||||
|
||||
# Verify host 1 and 2 objects were removed
|
||||
- name: Assert hosts were removed
|
||||
assert:
|
||||
that: item not in host_side_port_labels
|
||||
msg: "Host {{ item }} failed to be removed from the hosts!"
|
||||
loop: "{{ removed_host_side_port_labels }}"
|
@ -0,0 +1,190 @@
|
||||
# (c) 2018, NetApp Inc.
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from mock import MagicMock
|
||||
|
||||
from ansible.module_utils import basic, netapp
|
||||
from ansible.modules.storage.netapp import netapp_e_host
|
||||
from ansible.modules.storage.netapp.netapp_e_host import Host
|
||||
from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
|
||||
|
||||
__metaclass__ = type
|
||||
import unittest
|
||||
import mock
|
||||
import pytest
|
||||
import json
|
||||
from ansible.compat.tests.mock import patch
|
||||
from ansible.module_utils._text import to_bytes
|
||||
|
||||
|
||||
class HostTest(ModuleTestCase):
|
||||
REQUIRED_PARAMS = {
|
||||
'api_username': 'rw',
|
||||
'api_password': 'password',
|
||||
'api_url': 'http://localhost',
|
||||
'ssid': '1',
|
||||
'name': '1',
|
||||
}
|
||||
HOST = {
|
||||
'name': '1',
|
||||
'label': '1',
|
||||
'id': '0' * 30,
|
||||
'clusterRef': 40 * '0',
|
||||
'hostTypeIndex': 28,
|
||||
'hostSidePorts': [],
|
||||
'initiators': [],
|
||||
'ports': [],
|
||||
}
|
||||
HOST_ALT = {
|
||||
'name': '2',
|
||||
'label': '2',
|
||||
'id': '1' * 30,
|
||||
'clusterRef': '1',
|
||||
'hostSidePorts': [],
|
||||
'initiators': [],
|
||||
'ports': [],
|
||||
}
|
||||
REQ_FUNC = 'ansible.modules.storage.netapp.netapp_e_host.request'
|
||||
|
||||
def _set_args(self, args):
|
||||
module_args = self.REQUIRED_PARAMS.copy()
|
||||
module_args.update(args)
|
||||
set_module_args(module_args)
|
||||
|
||||
def test_delete_host(self):
|
||||
"""Validate removing a host object"""
|
||||
self._set_args({
|
||||
'state': 'absent'
|
||||
})
|
||||
host = Host()
|
||||
with self.assertRaises(AnsibleExitJson) as result:
|
||||
# We expect 2 calls to the API, the first to retrieve the host objects defined,
|
||||
# the second to remove the host definition.
|
||||
with mock.patch(self.REQ_FUNC, side_effect=[(200, [self.HOST]), (204, {})]) as request:
|
||||
host.apply()
|
||||
self.assertEquals(request.call_count, 2)
|
||||
# We expect the module to make changes
|
||||
self.assertEquals(result.exception.args[0]['changed'], True)
|
||||
|
||||
def test_delete_host_no_changes(self):
|
||||
"""Ensure that removing a host that doesn't exist works correctly."""
|
||||
self._set_args({
|
||||
'state': 'absent'
|
||||
})
|
||||
host = Host()
|
||||
with self.assertRaises(AnsibleExitJson) as result:
|
||||
# We expect a single call to the API: retrieve the defined hosts.
|
||||
with mock.patch(self.REQ_FUNC, return_value=(200, [])):
|
||||
host.apply()
|
||||
# We should not mark changed=True
|
||||
self.assertEquals(result.exception.args[0]['changed'], False)
|
||||
|
||||
def test_host_exists(self):
|
||||
"""Test host_exists method"""
|
||||
self._set_args({
|
||||
'state': 'absent'
|
||||
})
|
||||
host = Host()
|
||||
with mock.patch(self.REQ_FUNC, return_value=(200, [self.HOST])) as request:
|
||||
host_exists = host.host_exists
|
||||
self.assertTrue(host_exists, msg="This host should exist!")
|
||||
|
||||
def test_host_exists_negative(self):
|
||||
"""Test host_exists method with no matching hosts to return"""
|
||||
self._set_args({
|
||||
'state': 'absent'
|
||||
})
|
||||
host = Host()
|
||||
with mock.patch(self.REQ_FUNC, return_value=(200, [self.HOST_ALT])) as request:
|
||||
host_exists = host.host_exists
|
||||
self.assertFalse(host_exists, msg="This host should exist!")
|
||||
|
||||
def test_host_exists_fail(self):
|
||||
"""Ensure we do not dump a stack trace if we fail to make the request"""
|
||||
self._set_args({
|
||||
'state': 'absent'
|
||||
})
|
||||
host = Host()
|
||||
with self.assertRaises(AnsibleFailJson):
|
||||
with mock.patch(self.REQ_FUNC, side_effect=Exception("http_error")) as request:
|
||||
host_exists = host.host_exists
|
||||
|
||||
def test_needs_update_host_type(self):
|
||||
"""Ensure a changed host_type triggers an update"""
|
||||
self._set_args({
|
||||
'state': 'present',
|
||||
'host_type': 27
|
||||
})
|
||||
host = Host()
|
||||
host.host_obj = self.HOST
|
||||
with mock.patch(self.REQ_FUNC, return_value=(200, [self.HOST])) as request:
|
||||
needs_update = host.needs_update
|
||||
self.assertTrue(needs_update, msg="An update to the host should be required!")
|
||||
|
||||
def test_needs_update_cluster(self):
|
||||
"""Ensure a changed group_id triggers an update"""
|
||||
self._set_args({
|
||||
'state': 'present',
|
||||
'host_type': self.HOST['hostTypeIndex'],
|
||||
'group': '1',
|
||||
})
|
||||
host = Host()
|
||||
host.host_obj = self.HOST
|
||||
with mock.patch(self.REQ_FUNC, return_value=(200, [self.HOST])) as request:
|
||||
needs_update = host.needs_update
|
||||
self.assertTrue(needs_update, msg="An update to the host should be required!")
|
||||
|
||||
def test_needs_update_no_change(self):
|
||||
"""Ensure no changes do not trigger an update"""
|
||||
self._set_args({
|
||||
'state': 'present',
|
||||
'host_type': self.HOST['hostTypeIndex'],
|
||||
})
|
||||
host = Host()
|
||||
host.host_obj = self.HOST
|
||||
with mock.patch(self.REQ_FUNC, return_value=(200, [self.HOST])) as request:
|
||||
needs_update = host.needs_update
|
||||
self.assertFalse(needs_update, msg="An update to the host should be required!")
|
||||
|
||||
def test_needs_update_ports(self):
|
||||
"""Ensure added ports trigger an update"""
|
||||
self._set_args({
|
||||
'state': 'present',
|
||||
'host_type': self.HOST['hostTypeIndex'],
|
||||
'ports': [{'label': 'abc', 'type': 'iscsi', 'port': '0'}],
|
||||
})
|
||||
host = Host()
|
||||
host.host_obj = self.HOST
|
||||
with mock.patch.object(host, 'all_hosts', [self.HOST]):
|
||||
needs_update = host.needs_update
|
||||
self.assertTrue(needs_update, msg="An update to the host should be required!")
|
||||
|
||||
def test_needs_update_changed_ports(self):
|
||||
"""Ensure changed ports trigger an update"""
|
||||
self._set_args({
|
||||
'state': 'present',
|
||||
'host_type': self.HOST['hostTypeIndex'],
|
||||
'ports': [{'label': 'abc', 'type': 'iscsi', 'port': '0'}],
|
||||
})
|
||||
host = Host()
|
||||
host.host_obj = self.HOST.copy()
|
||||
host.host_obj['hostSidePorts'] = [{'label': 'xyz', 'type': 'iscsi', 'port': '0', 'address': 'iqn:0'}]
|
||||
|
||||
with mock.patch.object(host, 'all_hosts', [self.HOST]):
|
||||
needs_update = host.needs_update
|
||||
self.assertTrue(needs_update, msg="An update to the host should be required!")
|
||||
|
||||
def test_needs_update_changed_negative(self):
|
||||
"""Ensure a ports update with no changes does not trigger an update"""
|
||||
self._set_args({
|
||||
'state': 'present',
|
||||
'host_type': self.HOST['hostTypeIndex'],
|
||||
'ports': [{'label': 'abc', 'type': 'iscsi', 'port': '0'}],
|
||||
})
|
||||
host = Host()
|
||||
host.host_obj = self.HOST.copy()
|
||||
host.host_obj['hostSidePorts'] = [{'label': 'xyz', 'type': 'iscsi', 'port': '0', 'address': 'iqn:0'}]
|
||||
|
||||
with mock.patch.object(host, 'all_hosts', [self.HOST]):
|
||||
needs_update = host.needs_update
|
||||
self.assertTrue(needs_update, msg="An update to the host should be required!")
|
Loading…
Reference in New Issue