New module for managing NetApp E-Series iSCSI Interfaces (#39877)
* New module for NTAP E-Series iSCSI Interfaces Define a new module for configuring NetApp E-Series iSCSI interfaces. * Improve netapp_e_iscsi_interface integration tests Restructured integration test to set all iscsi ports to disabled, then defines the ports either statically or with dhcp, next updates the ports with the other definition type (static <-> dhcp), and lastly disables all ports. Each netapp_eseries_iscsi_interface call is verified with the array.
This commit is contained in:
parent
d38bccfc3e
commit
97157cf876
5 changed files with 1112 additions and 0 deletions
398
lib/ansible/modules/storage/netapp/netapp_e_iscsi_interface.py
Normal file
398
lib/ansible/modules/storage/netapp/netapp_e_iscsi_interface.py
Normal file
|
@ -0,0 +1,398 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
# (c) 2018, NetApp, Inc
|
||||||
|
# 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.1',
|
||||||
|
'status': ['preview'],
|
||||||
|
'supported_by': 'community'}
|
||||||
|
|
||||||
|
DOCUMENTATION = """
|
||||||
|
---
|
||||||
|
module: netapp_e_iscsi_interface
|
||||||
|
short_description: NetApp E-Series manage iSCSI interface configuration
|
||||||
|
description:
|
||||||
|
- Configure settings of an E-Series iSCSI interface
|
||||||
|
version_added: '2.7'
|
||||||
|
author: Michael Price (@lmprice)
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- netapp.eseries
|
||||||
|
options:
|
||||||
|
controller:
|
||||||
|
description:
|
||||||
|
- The controller that owns the port you want to configure.
|
||||||
|
- Controller names are presented alphabetically, with the first controller as A,
|
||||||
|
the second as B, and so on.
|
||||||
|
- Current hardware models have either 1 or 2 available controllers, but that is not a guaranteed hard
|
||||||
|
limitation and could change in the future.
|
||||||
|
required: yes
|
||||||
|
choices:
|
||||||
|
- A
|
||||||
|
- B
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- The channel of the port to modify the configuration of.
|
||||||
|
- The list of choices is not necessarily comprehensive. It depends on the number of ports
|
||||||
|
that are available in the system.
|
||||||
|
- The numerical value represents the number of the channel (typically from left to right on the HIC),
|
||||||
|
beginning with a value of 1.
|
||||||
|
required: yes
|
||||||
|
aliases:
|
||||||
|
- channel
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- When enabled, the provided configuration will be utilized.
|
||||||
|
- When disabled, the IPv4 configuration will be cleared and IPv4 connectivity disabled.
|
||||||
|
choices:
|
||||||
|
- enabled
|
||||||
|
- disabled
|
||||||
|
default: enabled
|
||||||
|
address:
|
||||||
|
description:
|
||||||
|
- The IPv4 address to assign to the interface.
|
||||||
|
- Should be specified in xx.xx.xx.xx form.
|
||||||
|
- Mutually exclusive with I(config_method=dhcp)
|
||||||
|
subnet_mask:
|
||||||
|
description:
|
||||||
|
- The subnet mask to utilize for the interface.
|
||||||
|
- Should be specified in xx.xx.xx.xx form.
|
||||||
|
- Mutually exclusive with I(config_method=dhcp)
|
||||||
|
gateway:
|
||||||
|
description:
|
||||||
|
- The IPv4 gateway address to utilize for the interface.
|
||||||
|
- Should be specified in xx.xx.xx.xx form.
|
||||||
|
- Mutually exclusive with I(config_method=dhcp)
|
||||||
|
config_method:
|
||||||
|
description:
|
||||||
|
- The configuration method type to use for this interface.
|
||||||
|
- dhcp is mutually exclusive with I(address), I(subnet_mask), and I(gateway).
|
||||||
|
choices:
|
||||||
|
- dhcp
|
||||||
|
- static
|
||||||
|
default: dhcp
|
||||||
|
mtu:
|
||||||
|
description:
|
||||||
|
- The maximum transmission units (MTU), in bytes.
|
||||||
|
- This allows you to configure a larger value for the MTU, in order to enable jumbo frames
|
||||||
|
(any value > 1500).
|
||||||
|
- Generally, it is necessary to have your host, switches, and other components not only support jumbo
|
||||||
|
frames, but also have it configured properly. Therefore, unless you know what you're doing, it's best to
|
||||||
|
leave this at the default.
|
||||||
|
default: 1500
|
||||||
|
aliases:
|
||||||
|
- max_frame_size
|
||||||
|
log_path:
|
||||||
|
description:
|
||||||
|
- A local path to a file to be used for debug logging
|
||||||
|
required: no
|
||||||
|
notes:
|
||||||
|
- Check mode is supported.
|
||||||
|
- The interface settings are applied synchronously, but changes to the interface itself (receiving a new IP address
|
||||||
|
via dhcp, etc), can take seconds or minutes longer to take effect.
|
||||||
|
- This module will not be useful/usable on an E-Series system without any iSCSI interfaces.
|
||||||
|
- This module requires a Web Services API version of >= 1.3.
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = """
|
||||||
|
- name: Configure the first port on the A controller with a static IPv4 address
|
||||||
|
netapp_e_iscsi_interface:
|
||||||
|
name: "1"
|
||||||
|
controller: "A"
|
||||||
|
config_method: static
|
||||||
|
address: "192.168.1.100"
|
||||||
|
subnet_mask: "255.255.255.0"
|
||||||
|
gateway: "192.168.1.1"
|
||||||
|
ssid: "1"
|
||||||
|
api_url: "10.1.1.1:8443"
|
||||||
|
api_username: "admin"
|
||||||
|
api_password: "myPass"
|
||||||
|
|
||||||
|
- name: Disable ipv4 connectivity for the second port on the B controller
|
||||||
|
netapp_e_iscsi_interface:
|
||||||
|
name: "2"
|
||||||
|
controller: "B"
|
||||||
|
state: disabled
|
||||||
|
ssid: "{{ ssid }}"
|
||||||
|
api_url: "{{ netapp_api_url }}"
|
||||||
|
api_username: "{{ netapp_api_username }}"
|
||||||
|
api_password: "{{ netapp_api_password }}"
|
||||||
|
|
||||||
|
- name: Enable jumbo frames for the first 4 ports on controller A
|
||||||
|
netapp_e_iscsi_interface:
|
||||||
|
name: "{{ item | int }}"
|
||||||
|
controller: "A"
|
||||||
|
state: enabled
|
||||||
|
mtu: 9000
|
||||||
|
config_method: dhcp
|
||||||
|
ssid: "{{ ssid }}"
|
||||||
|
api_url: "{{ netapp_api_url }}"
|
||||||
|
api_username: "{{ netapp_api_username }}"
|
||||||
|
api_password: "{{ netapp_api_password }}"
|
||||||
|
loop:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- 3
|
||||||
|
- 4
|
||||||
|
"""
|
||||||
|
|
||||||
|
RETURN = """
|
||||||
|
msg:
|
||||||
|
description: Success message
|
||||||
|
returned: on success
|
||||||
|
type: string
|
||||||
|
sample: The interface settings have been updated.
|
||||||
|
enabled:
|
||||||
|
description:
|
||||||
|
- Indicates whether IPv4 connectivity has been enabled or disabled.
|
||||||
|
- This does not necessarily indicate connectivity. If dhcp was enabled without a dhcp server, for instance,
|
||||||
|
it is unlikely that the configuration will actually be valid.
|
||||||
|
returned: on success
|
||||||
|
sample: True
|
||||||
|
type: bool
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from pprint import pformat
|
||||||
|
import re
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
from ansible.module_utils.netapp import request, eseries_host_argument_spec
|
||||||
|
from ansible.module_utils._text import to_native
|
||||||
|
|
||||||
|
HEADERS = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class IscsiInterface(object):
|
||||||
|
def __init__(self):
|
||||||
|
argument_spec = eseries_host_argument_spec()
|
||||||
|
argument_spec.update(dict(
|
||||||
|
controller=dict(type='str', required=True, choices=['A', 'B']),
|
||||||
|
name=dict(type='int', aliases=['channel']),
|
||||||
|
state=dict(type='str', required=False, default='enabled', choices=['enabled', 'disabled']),
|
||||||
|
address=dict(type='str', required=False),
|
||||||
|
subnet_mask=dict(type='str', required=False),
|
||||||
|
gateway=dict(type='str', required=False),
|
||||||
|
config_method=dict(type='str', required=False, default='dhcp', choices=['dhcp', 'static']),
|
||||||
|
mtu=dict(type='int', default=1500, required=False, aliases=['max_frame_size']),
|
||||||
|
log_path=dict(type='str', required=False),
|
||||||
|
))
|
||||||
|
|
||||||
|
required_if = [
|
||||||
|
["config_method", "static", ["address", "subnet_mask"]],
|
||||||
|
]
|
||||||
|
|
||||||
|
self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if, )
|
||||||
|
args = self.module.params
|
||||||
|
self.controller = args['controller']
|
||||||
|
self.name = args['name']
|
||||||
|
self.mtu = args['mtu']
|
||||||
|
self.state = args['state']
|
||||||
|
self.address = args['address']
|
||||||
|
self.subnet_mask = args['subnet_mask']
|
||||||
|
self.gateway = args['gateway']
|
||||||
|
self.config_method = args['config_method']
|
||||||
|
|
||||||
|
self.ssid = args['ssid']
|
||||||
|
self.url = args['api_url']
|
||||||
|
self.creds = dict(url_password=args['api_password'],
|
||||||
|
validate_certs=args['validate_certs'],
|
||||||
|
url_username=args['api_username'], )
|
||||||
|
|
||||||
|
self.check_mode = self.module.check_mode
|
||||||
|
self.post_body = dict()
|
||||||
|
self.controllers = list()
|
||||||
|
|
||||||
|
log_path = args['log_path']
|
||||||
|
|
||||||
|
# logging setup
|
||||||
|
self._logger = logging.getLogger(self.__class__.__name__)
|
||||||
|
|
||||||
|
if log_path:
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.DEBUG, filename=log_path, filemode='w',
|
||||||
|
format='%(relativeCreated)dms %(levelname)s %(module)s.%(funcName)s:%(lineno)d\n %(message)s')
|
||||||
|
|
||||||
|
if not self.url.endswith('/'):
|
||||||
|
self.url += '/'
|
||||||
|
|
||||||
|
if self.mtu < 1500 or self.mtu > 9000:
|
||||||
|
self.module.fail_json(msg="The provided mtu is invalid, it must be > 1500 and < 9000 bytes.")
|
||||||
|
|
||||||
|
if self.config_method == 'dhcp' and any([self.address, self.subnet_mask, self.gateway]):
|
||||||
|
self.module.fail_json(
|
||||||
|
'A config_method of dhcp is mutually exclusive with the address, subnet_mask, and gateway options.')
|
||||||
|
|
||||||
|
# A relatively primitive regex to validate that the input is formatted like a valid ip address
|
||||||
|
address_regex = re.compile(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}')
|
||||||
|
|
||||||
|
if self.address and not address_regex.match(self.address):
|
||||||
|
self.module.fail_json("An invalid ip address was provided for address.")
|
||||||
|
|
||||||
|
if self.subnet_mask and not address_regex.match(self.subnet_mask):
|
||||||
|
self.module.fail_json("An invalid ip address was provided for subnet_mask.")
|
||||||
|
|
||||||
|
if self.gateway and not address_regex.match(self.gateway):
|
||||||
|
self.module.fail_json("An invalid ip address was provided for gateway.")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def interfaces(self):
|
||||||
|
ifaces = list()
|
||||||
|
try:
|
||||||
|
(rc, ifaces) = request(self.url + 'storage-systems/%s/graph/xpath-filter?query=/controller/hostInterfaces'
|
||||||
|
% self.ssid, headers=HEADERS, **self.creds)
|
||||||
|
except Exception as err:
|
||||||
|
self.module.fail_json(
|
||||||
|
msg="Failed to retrieve defined host interfaces. Array Id [%s]. Error [%s]."
|
||||||
|
% (self.ssid, to_native(err)))
|
||||||
|
|
||||||
|
# Filter out non-iSCSI interfaces
|
||||||
|
ifaces = [iface['iscsi'] for iface in ifaces if iface['interfaceType'] == 'iscsi']
|
||||||
|
|
||||||
|
return ifaces
|
||||||
|
|
||||||
|
def get_controllers(self):
|
||||||
|
"""Retrieve a mapping of controller labels to their references
|
||||||
|
{
|
||||||
|
'A': '070000000000000000000001',
|
||||||
|
'B': '070000000000000000000002',
|
||||||
|
}
|
||||||
|
:return: the controllers defined on the system
|
||||||
|
"""
|
||||||
|
controllers = list()
|
||||||
|
try:
|
||||||
|
(rc, controllers) = request(self.url + 'storage-systems/%s/graph/xpath-filter?query=/controller/id'
|
||||||
|
% self.ssid, headers=HEADERS, **self.creds)
|
||||||
|
except Exception as err:
|
||||||
|
self.module.fail_json(
|
||||||
|
msg="Failed to retrieve controller list! Array Id [%s]. Error [%s]."
|
||||||
|
% (self.ssid, to_native(err)))
|
||||||
|
|
||||||
|
controllers.sort()
|
||||||
|
|
||||||
|
controllers_dict = {}
|
||||||
|
i = ord('A')
|
||||||
|
for controller in controllers:
|
||||||
|
label = chr(i)
|
||||||
|
controllers_dict[label] = controller
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return controllers_dict
|
||||||
|
|
||||||
|
def fetch_target_interface(self):
|
||||||
|
interfaces = self.interfaces
|
||||||
|
|
||||||
|
for iface in interfaces:
|
||||||
|
if iface['channel'] == self.name and self.controllers[self.controller] == iface['controllerId']:
|
||||||
|
return iface
|
||||||
|
|
||||||
|
channels = sorted(set((str(iface['channel'])) for iface in interfaces
|
||||||
|
if self.controllers[self.controller] == iface['controllerId']))
|
||||||
|
|
||||||
|
self.module.fail_json(msg="The requested channel of %s is not valid. Valid channels include: %s."
|
||||||
|
% (self.name, ", ".join(channels)))
|
||||||
|
|
||||||
|
def make_update_body(self, target_iface):
|
||||||
|
body = dict(iscsiInterface=target_iface['id'])
|
||||||
|
update_required = False
|
||||||
|
|
||||||
|
self._logger.info("Requested state=%s.", self.state)
|
||||||
|
self._logger.info("config_method: current=%s, requested=%s",
|
||||||
|
target_iface['ipv4Data']['ipv4AddressConfigMethod'], self.config_method)
|
||||||
|
|
||||||
|
if self.state == 'enabled':
|
||||||
|
settings = dict()
|
||||||
|
if not target_iface['ipv4Enabled']:
|
||||||
|
update_required = True
|
||||||
|
settings['ipv4Enabled'] = [True]
|
||||||
|
if self.mtu != target_iface['interfaceData']['ethernetData']['maximumFramePayloadSize']:
|
||||||
|
update_required = True
|
||||||
|
settings['maximumFramePayloadSize'] = [self.mtu]
|
||||||
|
if self.config_method == 'static':
|
||||||
|
ipv4Data = target_iface['ipv4Data']['ipv4AddressData']
|
||||||
|
|
||||||
|
if ipv4Data['ipv4Address'] != self.address:
|
||||||
|
update_required = True
|
||||||
|
settings['ipv4Address'] = [self.address]
|
||||||
|
if ipv4Data['ipv4SubnetMask'] != self.subnet_mask:
|
||||||
|
update_required = True
|
||||||
|
settings['ipv4SubnetMask'] = [self.subnet_mask]
|
||||||
|
if self.gateway is not None and ipv4Data['ipv4GatewayAddress'] != self.gateway:
|
||||||
|
update_required = True
|
||||||
|
settings['ipv4GatewayAddress'] = [self.gateway]
|
||||||
|
|
||||||
|
if target_iface['ipv4Data']['ipv4AddressConfigMethod'] != 'configStatic':
|
||||||
|
update_required = True
|
||||||
|
settings['ipv4AddressConfigMethod'] = ['configStatic']
|
||||||
|
|
||||||
|
elif (target_iface['ipv4Data']['ipv4AddressConfigMethod'] != 'configDhcp'):
|
||||||
|
update_required = True
|
||||||
|
settings.update(dict(ipv4Enabled=[True],
|
||||||
|
ipv4AddressConfigMethod=['configDhcp']))
|
||||||
|
body['settings'] = settings
|
||||||
|
|
||||||
|
else:
|
||||||
|
if target_iface['ipv4Enabled']:
|
||||||
|
update_required = True
|
||||||
|
body['settings'] = dict(ipv4Enabled=[False])
|
||||||
|
|
||||||
|
self._logger.info("Update required ?=%s", update_required)
|
||||||
|
self._logger.info("Update body: %s", pformat(body))
|
||||||
|
|
||||||
|
return update_required, body
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
self.controllers = self.get_controllers()
|
||||||
|
if self.controller not in self.controllers:
|
||||||
|
self.module.fail_json(msg="The provided controller name is invalid. Valid controllers: %s."
|
||||||
|
% ", ".join(self.controllers.keys()))
|
||||||
|
|
||||||
|
iface_before = self.fetch_target_interface()
|
||||||
|
update_required, body = self.make_update_body(iface_before)
|
||||||
|
if update_required and not self.check_mode:
|
||||||
|
try:
|
||||||
|
url = (self.url +
|
||||||
|
'storage-systems/%s/symbol/setIscsiInterfaceProperties' % self.ssid)
|
||||||
|
(rc, result) = request(url, method='POST', data=json.dumps(body), headers=HEADERS, timeout=300,
|
||||||
|
ignore_errors=True, **self.creds)
|
||||||
|
# We could potentially retry this a few times, but it's probably a rare enough case (unless a playbook
|
||||||
|
# is cancelled mid-flight), that it isn't worth the complexity.
|
||||||
|
if rc == 422 and result['retcode'] in ['busy', '3']:
|
||||||
|
self.module.fail_json(
|
||||||
|
msg="The interface is currently busy (probably processing a previously requested modification"
|
||||||
|
" request). This operation cannot currently be completed. Array Id [%s]. Error [%s]."
|
||||||
|
% (self.ssid, result))
|
||||||
|
# Handle authentication issues, etc.
|
||||||
|
elif rc != 200:
|
||||||
|
self.module.fail_json(
|
||||||
|
msg="Failed to modify the interface! Array Id [%s]. Error [%s]."
|
||||||
|
% (self.ssid, to_native(result)))
|
||||||
|
self._logger.debug("Update request completed successfully.")
|
||||||
|
# This is going to catch cases like a connection failure
|
||||||
|
except Exception as err:
|
||||||
|
self.module.fail_json(
|
||||||
|
msg="Connection failure: we failed to modify the interface! Array Id [%s]. Error [%s]."
|
||||||
|
% (self.ssid, to_native(err)))
|
||||||
|
|
||||||
|
iface_after = self.fetch_target_interface()
|
||||||
|
|
||||||
|
self.module.exit_json(msg="The interface settings have been updated.", changed=update_required,
|
||||||
|
enabled=iface_after['ipv4Enabled'])
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
iface = IscsiInterface()
|
||||||
|
iface()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -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,448 @@
|
||||||
|
---
|
||||||
|
# Test code for the netapp_e_iscsi_interface module
|
||||||
|
# (c) 2018, NetApp, Inc
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
|
||||||
|
# ***********************
|
||||||
|
# *** Local test data ***
|
||||||
|
# ***********************
|
||||||
|
- name: NetApp Test iSCSI Interface 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:
|
||||||
|
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
|
||||||
|
array: &array
|
||||||
|
subnet: 255.255.255.0
|
||||||
|
gateway: 10.10.10.1
|
||||||
|
A:
|
||||||
|
- channel: 1
|
||||||
|
max_frame_size: 1500
|
||||||
|
- channel: 2
|
||||||
|
max_frame_size: 2000
|
||||||
|
- channel: 3
|
||||||
|
max_frame_size: 9000
|
||||||
|
- channel: 4
|
||||||
|
max_frame_size: 1500
|
||||||
|
- channel: 5
|
||||||
|
max_frame_size: 2000
|
||||||
|
- channel: 6
|
||||||
|
max_frame_size: 9000
|
||||||
|
B:
|
||||||
|
- channel: 1
|
||||||
|
max_frame_size: 9000
|
||||||
|
- channel: 2
|
||||||
|
max_frame_size: 1500
|
||||||
|
- channel: 3
|
||||||
|
max_frame_size: 2000
|
||||||
|
- channel: 4
|
||||||
|
max_frame_size: 9000
|
||||||
|
- channel: 5
|
||||||
|
max_frame_size: 1500
|
||||||
|
- channel: 6
|
||||||
|
max_frame_size: 2000
|
||||||
|
|
||||||
|
|
||||||
|
# ***************************************************
|
||||||
|
# *** Ensure python jmespath package is installed ***
|
||||||
|
# ***************************************************
|
||||||
|
- name: Ensure that jmespath is installed
|
||||||
|
pip:
|
||||||
|
name: jmespath
|
||||||
|
state: enabled
|
||||||
|
register: jmespath
|
||||||
|
- fail:
|
||||||
|
msg: "Restart playbook, the jmespath package was installed and is need for the playbook's execution."
|
||||||
|
when: jmespath.changed
|
||||||
|
|
||||||
|
|
||||||
|
# ************************************
|
||||||
|
# *** Set local playbook test data ***
|
||||||
|
# ************************************
|
||||||
|
- name: set credentials
|
||||||
|
set_fact:
|
||||||
|
credentials: *creds
|
||||||
|
- name: set array
|
||||||
|
set_fact:
|
||||||
|
array: *array
|
||||||
|
|
||||||
|
- name: Show some debug information
|
||||||
|
debug:
|
||||||
|
msg: "Using user={{ credentials.api_username }} on server={{ credentials.api_url }}."
|
||||||
|
|
||||||
|
|
||||||
|
# *****************************************
|
||||||
|
# *** Disable all controller A channels ***
|
||||||
|
# *****************************************
|
||||||
|
- name: Disable all controller A ports
|
||||||
|
netapp_e_iscsi_interface:
|
||||||
|
<<: *creds
|
||||||
|
controller: "A"
|
||||||
|
channel: "{{ item.channel }}"
|
||||||
|
state: disabled
|
||||||
|
loop: "{{ lookup('list', array.A) }}"
|
||||||
|
|
||||||
|
# Delay to give time for the asynchronous symbol call has complete
|
||||||
|
- pause:
|
||||||
|
seconds: 30
|
||||||
|
|
||||||
|
# Request all controller's iscsi host interface information
|
||||||
|
- name: Collect iscsi port information
|
||||||
|
uri:
|
||||||
|
url: "{{ xpath_filter_url }}?query=controller/hostInterfaces//iscsi"
|
||||||
|
user: "{{ credentials.api_username }}"
|
||||||
|
password: "{{ credentials.api_password }}"
|
||||||
|
body_format: json
|
||||||
|
validate_certs: no
|
||||||
|
register: result
|
||||||
|
vars:
|
||||||
|
xpath_filter_url: "{{ credentials.api_url }}/storage-systems/{{ credentials.ssid }}/graph/xpath-filter"
|
||||||
|
|
||||||
|
# Extract controller A's port information from the iscsi host interfaces list
|
||||||
|
# Note: min filter is used because there are only two controller ids and the smaller corresponds with controller A
|
||||||
|
- name: Get controller A's controllerId
|
||||||
|
set_fact:
|
||||||
|
controller_a_id: "{{ result | json_query('json[*].controllerId') | min }}"
|
||||||
|
|
||||||
|
# Collect all port information associated with controller A
|
||||||
|
- name: Get controller A's port information
|
||||||
|
set_fact:
|
||||||
|
controller_a: "{{ result | json_query(controller_a_query) }}"
|
||||||
|
vars:
|
||||||
|
controller_a_query: "json[?controllerId=='{{ controller_a_id }}']"
|
||||||
|
|
||||||
|
# Confirm controller A's ports are disabled
|
||||||
|
- name: Verify all controller A ports are disabled
|
||||||
|
assert:
|
||||||
|
that: "{{ item.ipv4Enabled == false }}"
|
||||||
|
msg: "Controller A, channel {{ item.channel }} is not disabled"
|
||||||
|
loop: "{{ controller_a }}"
|
||||||
|
|
||||||
|
|
||||||
|
# *****************************************
|
||||||
|
# *** Disable all controller B channels ***
|
||||||
|
# *****************************************
|
||||||
|
- name: Disable all controller B ports
|
||||||
|
netapp_e_iscsi_interface:
|
||||||
|
<<: *creds
|
||||||
|
controller: "B"
|
||||||
|
channel: "{{ item.channel }}"
|
||||||
|
state: disabled
|
||||||
|
loop: "{{ lookup('list', array.B) }}"
|
||||||
|
|
||||||
|
# Delay to give time for the asynchronous symbol call has complete
|
||||||
|
- pause:
|
||||||
|
seconds: 30
|
||||||
|
|
||||||
|
# Request all controller's iscsi host interface information
|
||||||
|
- name: Collect iscsi port information
|
||||||
|
uri:
|
||||||
|
url: "{{ xpath_filter_url }}?query=controller/hostInterfaces//iscsi"
|
||||||
|
user: "{{ credentials.api_username }}"
|
||||||
|
password: "{{ credentials.api_password }}"
|
||||||
|
body_format: json
|
||||||
|
validate_certs: no
|
||||||
|
register: result
|
||||||
|
vars:
|
||||||
|
xpath_filter_url: "{{ credentials.api_url }}/storage-systems/{{ credentials.ssid }}/graph/xpath-filter"
|
||||||
|
|
||||||
|
# Extract controller B's port information from the iscsi host interfaces list
|
||||||
|
# Note: min filter is used because there are only two controller ids and the smaller corresponds with controller B
|
||||||
|
- name: Get controller B's controllerId
|
||||||
|
set_fact:
|
||||||
|
controller_b_id: "{{ result | json_query('json[*].controllerId') | max }}"
|
||||||
|
|
||||||
|
# Collect all port information associated with controller B
|
||||||
|
- name: Get controller B's port information
|
||||||
|
set_fact:
|
||||||
|
controller_b: "{{ result | json_query(controller_b_query) }}"
|
||||||
|
vars:
|
||||||
|
controller_b_query: "json[?controllerId=='{{ controller_b_id }}']"
|
||||||
|
|
||||||
|
# Confirm controller B's ports are disabled
|
||||||
|
- name: Verify all controller B ports are disabled
|
||||||
|
assert:
|
||||||
|
that: "{{ item.ipv4Enabled == false }}"
|
||||||
|
msg: "Controller B, channel {{ item.channel }} is not disabled"
|
||||||
|
loop: "{{ controller_b }}"
|
||||||
|
|
||||||
|
|
||||||
|
# *****************************************************
|
||||||
|
# *** Configure all controller A's ports statically ***
|
||||||
|
# *****************************************************
|
||||||
|
- name: Configure controller A's port to use a static configuration method
|
||||||
|
netapp_e_iscsi_interface:
|
||||||
|
<<: *creds
|
||||||
|
controller: "A"
|
||||||
|
channel: "{{ item.channel }}"
|
||||||
|
state: enabled
|
||||||
|
config_method: static
|
||||||
|
address: "{{ array.gateway.split('.')[:3] | join('.') }}.{{ item.channel }}"
|
||||||
|
subnet_mask: "{{ array.subnet }}"
|
||||||
|
gateway: "{{ array.gateway }}"
|
||||||
|
max_frame_size: "{{ item.max_frame_size }}"
|
||||||
|
loop: "{{ lookup('list', array.A) }}"
|
||||||
|
|
||||||
|
# Delay to give time for the asynchronous symbol call has complete
|
||||||
|
- pause:
|
||||||
|
seconds: 30
|
||||||
|
|
||||||
|
# Request a list of iscsi host interfaces
|
||||||
|
- name: Collect array information
|
||||||
|
uri:
|
||||||
|
url: "{{ xpath_filter }}?query=controller/hostInterfaces//iscsi"
|
||||||
|
user: "{{ credentials.api_username }}"
|
||||||
|
password: "{{ credentials.api_password }}"
|
||||||
|
body_format: json
|
||||||
|
validate_certs: no
|
||||||
|
register: result
|
||||||
|
vars:
|
||||||
|
xpath_filter: "{{ credentials.api_url }}/storage-systems/{{ credentials.ssid }}/graph/xpath-filter"
|
||||||
|
|
||||||
|
# Extract controller A's port information from the iscsi host interfaces list
|
||||||
|
# Note: min filter is used because there are only two controller ids and the smaller corresponds with controller A
|
||||||
|
- name: Get controller A's controllerId
|
||||||
|
set_fact:
|
||||||
|
controller_a_id: "{{ result | json_query('json[*].controllerId') | min }}"
|
||||||
|
|
||||||
|
# Compile any iscsi port information associated with controller A
|
||||||
|
- name: Get controller A's port information
|
||||||
|
set_fact:
|
||||||
|
controller_a: "{{ result | json_query(controller_a_query) }}"
|
||||||
|
vars:
|
||||||
|
controller_a_query: "json[?controllerId=='{{ controller_a_id }}']"
|
||||||
|
|
||||||
|
# Confirm that controller A ports are statically defined with the expected MTU, gateway, subnet and ipv4 address
|
||||||
|
- name: Verify expected controller A's port configuration
|
||||||
|
assert:
|
||||||
|
that: "{{ item[0].channel != item[1].channel or
|
||||||
|
( item[0].ipv4Data.ipv4AddressConfigMethod == 'configStatic' and
|
||||||
|
item[0].interfaceData.ethernetData.maximumFramePayloadSize == item[1].max_frame_size and
|
||||||
|
item[0].ipv4Data.ipv4AddressData.ipv4GatewayAddress == array.gateway and
|
||||||
|
item[0].ipv4Data.ipv4AddressData.ipv4SubnetMask == array.subnet and
|
||||||
|
item[0].ipv4Data.ipv4AddressData.ipv4Address == partial_address + item[1].channel | string ) }}"
|
||||||
|
msg: "Failed to configure controller A, channel {{ item[0].channel }}"
|
||||||
|
loop: "{{ query('nested', lookup('list', controller_a), lookup('list', array.A) ) }}"
|
||||||
|
vars:
|
||||||
|
partial_address: "{{ array.gateway.split('.')[:3] | join('.') + '.' }}"
|
||||||
|
|
||||||
|
|
||||||
|
# *******************************************************************************************
|
||||||
|
# *** Configure controller B's channels for dhcp and specific frame maximum payload sizes ***
|
||||||
|
# *******************************************************************************************
|
||||||
|
- name: Configure controller B's ports to use dhcp with different MTU
|
||||||
|
netapp_e_iscsi_interface:
|
||||||
|
<<: *creds
|
||||||
|
controller: "B"
|
||||||
|
channel: "{{ item.channel }}"
|
||||||
|
state: enabled
|
||||||
|
config_method: dhcp
|
||||||
|
max_frame_size: "{{ item.max_frame_size }}"
|
||||||
|
loop: "{{ lookup('list', array.B) }}"
|
||||||
|
|
||||||
|
# Delay to give time for the asynchronous symbol call has complete
|
||||||
|
- pause:
|
||||||
|
seconds: 30
|
||||||
|
|
||||||
|
# request a list of iscsi host interfaces
|
||||||
|
- name: Collect array information
|
||||||
|
uri:
|
||||||
|
url: "{{ xpath_filter_url }}?query=controller/hostInterfaces//iscsi"
|
||||||
|
user: "{{ credentials.api_username }}"
|
||||||
|
password: "{{ credentials.api_password }}"
|
||||||
|
body_format: json
|
||||||
|
validate_certs: no
|
||||||
|
register: result
|
||||||
|
vars:
|
||||||
|
xpath_filter_url: "{{ credentials.api_url }}/storage-systems/{{ credentials.ssid }}/graph/xpath-filter"
|
||||||
|
|
||||||
|
# Extract controller B's port information from the iscsi host interfaces list
|
||||||
|
# Note: max filter is used because there are only two controller ids and the larger corresponds with controller B
|
||||||
|
- name: Get controller B's controllerId
|
||||||
|
set_fact:
|
||||||
|
controller_b_id: "{{ result | json_query('json[*].controllerId') | max }}"
|
||||||
|
- name: Get controller B port information list
|
||||||
|
set_fact:
|
||||||
|
controller_b: "{{ result | json_query(controller_b_query) }}"
|
||||||
|
vars:
|
||||||
|
controller_b_query: "json[?controllerId=='{{ controller_b_id }}']"
|
||||||
|
|
||||||
|
# Using a nested loop of array information and expected information, verify that each channel has the appropriate max
|
||||||
|
# frame payload size and is configured for dhcp
|
||||||
|
- name: Verify expected controller B's port configuration
|
||||||
|
assert:
|
||||||
|
that: "{{ item[0].channel != item[1].channel or
|
||||||
|
( item[0].ipv4Data.ipv4AddressConfigMethod == 'configDhcp' and
|
||||||
|
item[0].interfaceData.ethernetData.maximumFramePayloadSize == item[1].max_frame_size ) }}"
|
||||||
|
msg: >
|
||||||
|
Failed to configure controller channel {{ item[0].channel }} for dhcp
|
||||||
|
and/or maximum frame size of {{ item[1].max_frame_size }}!
|
||||||
|
loop: "{{ query('nested', lookup('list', controller_b), lookup('list', array.B)) }}"
|
||||||
|
|
||||||
|
|
||||||
|
# *******************************************************************************************
|
||||||
|
# *** Configure controller A's channels for dhcp and specific frame maximum payload sizes ***
|
||||||
|
# *******************************************************************************************
|
||||||
|
- name: Configure controller A's ports to use dhcp with different MTU
|
||||||
|
netapp_e_iscsi_interface:
|
||||||
|
<<: *creds
|
||||||
|
controller: "A"
|
||||||
|
channel: "{{ item.channel }}"
|
||||||
|
state: enabled
|
||||||
|
config_method: dhcp
|
||||||
|
max_frame_size: "{{ item.max_frame_size }}"
|
||||||
|
loop: "{{ lookup('list', array.A) }}"
|
||||||
|
|
||||||
|
# Delay to give time for the asynchronous symbol call has complete
|
||||||
|
- pause:
|
||||||
|
seconds: 30
|
||||||
|
|
||||||
|
# Request a list of iscsi host interfaces
|
||||||
|
- name: Collect array information
|
||||||
|
uri:
|
||||||
|
url: "{{ xpath_filter_url }}?query=controller/hostInterfaces//iscsi"
|
||||||
|
user: "{{ credentials.api_username }}"
|
||||||
|
password: "{{ credentials.api_password }}"
|
||||||
|
body_format: json
|
||||||
|
validate_certs: no
|
||||||
|
register: result
|
||||||
|
vars:
|
||||||
|
xpath_filter_url: "{{ credentials.api_url }}/storage-systems/{{ credentials.ssid }}/graph/xpath-filter"
|
||||||
|
|
||||||
|
# Extract controller A's port information from the iscsi host interfaces list
|
||||||
|
# Note: min filter is used because there are only two controller ids and the larger corresponds with controller A
|
||||||
|
- name: Get controller A's controllerId
|
||||||
|
set_fact:
|
||||||
|
controller_a_id: "{{ result | json_query('json[*].controllerId') | min }}"
|
||||||
|
- name: Get controller A port information list
|
||||||
|
set_fact:
|
||||||
|
controller_a: "{{ result | json_query(controller_a_query) }}"
|
||||||
|
vars:
|
||||||
|
controller_a_query: "json[?controllerId=='{{ controller_a_id }}']"
|
||||||
|
|
||||||
|
# Using a nested loop of array information and expected information, verify that each channel has the appropriate max
|
||||||
|
# frame payload size and is configured for dhcp
|
||||||
|
- name: Verify expected controller A's port configuration
|
||||||
|
assert:
|
||||||
|
that: "{{ item[0].channel != item[1].channel or
|
||||||
|
( item[0].ipv4Data.ipv4AddressConfigMethod == 'configDhcp' and
|
||||||
|
item[0].interfaceData.ethernetData.maximumFramePayloadSize == item[1].max_frame_size ) }}"
|
||||||
|
msg: >
|
||||||
|
Failed to configure controller channel {{ item[0].channel }} for dhcp
|
||||||
|
and/or maximum frame size of {{ item[1].max_frame_size }}!
|
||||||
|
loop: "{{ query('nested', lookup('list', controller_a), lookup('list', array.A)) }}"
|
||||||
|
|
||||||
|
|
||||||
|
# *****************************************************
|
||||||
|
# *** Configure all controller B's ports statically ***
|
||||||
|
# *****************************************************
|
||||||
|
- name: Configure controller B's ports to use a static configuration method
|
||||||
|
netapp_e_iscsi_interface:
|
||||||
|
<<: *creds
|
||||||
|
controller: "B"
|
||||||
|
channel: "{{ item.channel }}"
|
||||||
|
state: enabled
|
||||||
|
config_method: static
|
||||||
|
address: "{{ array.gateway.split('.')[:3] | join('.') }}.{{ item.channel }}"
|
||||||
|
subnet_mask: "{{ array.subnet }}"
|
||||||
|
gateway: "{{ array.gateway }}"
|
||||||
|
max_frame_size: "{{ item.max_frame_size }}"
|
||||||
|
loop: "{{ lookup('list', array.B) }}"
|
||||||
|
|
||||||
|
# Delay to give time for the asynchronous symbol call has complete
|
||||||
|
- pause:
|
||||||
|
seconds: 30
|
||||||
|
|
||||||
|
# request a list of iscsi host interfaces
|
||||||
|
- name: Collect array information
|
||||||
|
uri:
|
||||||
|
url: "{{ xpath_filter }}?query=controller/hostInterfaces//iscsi"
|
||||||
|
user: "{{ credentials.api_username }}"
|
||||||
|
password: "{{ credentials.api_password }}"
|
||||||
|
body_format: json
|
||||||
|
validate_certs: no
|
||||||
|
register: result
|
||||||
|
vars:
|
||||||
|
xpath_filter: "{{ credentials.api_url }}/storage-systems/{{ credentials.ssid }}/graph/xpath-filter"
|
||||||
|
|
||||||
|
# Extract controller B's port information from the iscsi host interfaces list
|
||||||
|
# Note: min filter is used because there are only two controller ids and the smaller corresponds with controller B
|
||||||
|
- name: Get controller B's controllerId
|
||||||
|
set_fact:
|
||||||
|
controller_b_id: "{{ result | json_query('json[*].controllerId') | max }}"
|
||||||
|
|
||||||
|
# Compile any iscsi port information associated with controller B
|
||||||
|
- name: Get controller B's port information
|
||||||
|
set_fact:
|
||||||
|
controller_b: "{{ result | json_query(controller_b_query) }}"
|
||||||
|
vars:
|
||||||
|
controller_b_query: "json[?controllerId=='{{ controller_b_id }}']"
|
||||||
|
|
||||||
|
# Confirm that controller B ports are statically defined with the expected MTU, gateway, subnet and ipv4 address
|
||||||
|
- name: Verify expected controller B's port configuration
|
||||||
|
assert:
|
||||||
|
that: "{{ item[0].channel != item[1].channel or
|
||||||
|
( item[0].ipv4Data.ipv4AddressConfigMethod == 'configStatic' and
|
||||||
|
item[0].interfaceData.ethernetData.maximumFramePayloadSize == item[1].max_frame_size and
|
||||||
|
item[0].ipv4Data.ipv4AddressData.ipv4GatewayAddress == array.gateway and
|
||||||
|
item[0].ipv4Data.ipv4AddressData.ipv4SubnetMask == array.subnet and
|
||||||
|
item[0].ipv4Data.ipv4AddressData.ipv4Address == partial_address + item[1].channel | string ) }}"
|
||||||
|
msg: "Failed to configure controller B, channel {{ item[0].channel }}"
|
||||||
|
loop: "{{ query('nested', lookup('list', controller_b), lookup('list', array.B) ) }}"
|
||||||
|
vars:
|
||||||
|
partial_address: "{{ array.gateway.split('.')[:3] | join('.') + '.' }}"
|
||||||
|
|
||||||
|
|
||||||
|
# **************************************
|
||||||
|
# *** Disable all controller B ports ***
|
||||||
|
# **************************************
|
||||||
|
- name: Disable all controller B's ports
|
||||||
|
netapp_e_iscsi_interface:
|
||||||
|
<<: *creds
|
||||||
|
controller: "B"
|
||||||
|
channel: "{{ item.channel }}"
|
||||||
|
state: disabled
|
||||||
|
loop: "{{ lookup('list', array.B) }}"
|
||||||
|
|
||||||
|
# Delay to give time for the asynchronous symbol call has complete
|
||||||
|
- pause:
|
||||||
|
seconds: 30
|
||||||
|
|
||||||
|
# Request controller iscsi host interface information
|
||||||
|
- name: Collect iscsi port information
|
||||||
|
uri:
|
||||||
|
url: "{{ xpath_filter_url }}?query=controller/hostInterfaces//iscsi"
|
||||||
|
user: "{{ credentials.api_username }}"
|
||||||
|
password: "{{ credentials.api_password }}"
|
||||||
|
body_format: json
|
||||||
|
validate_certs: no
|
||||||
|
register: result
|
||||||
|
vars:
|
||||||
|
xpath_filter_url: "{{ credentials.api_url }}/storage-systems/{{ credentials.ssid }}/graph/xpath-filter"
|
||||||
|
|
||||||
|
# Extract controller A's port information from the iscsi host interfaces list
|
||||||
|
# Note: min filter is used because there are only two controller ids and the smaller corresponds with controller B
|
||||||
|
- name: Get controller B's controllerId
|
||||||
|
set_fact:
|
||||||
|
controller_b_id: "{{ result | json_query('json[*].controllerId') | max }}"
|
||||||
|
|
||||||
|
# Compile any iscsi port information associated with controller B
|
||||||
|
- name: Get controller B's port information
|
||||||
|
set_fact:
|
||||||
|
controller_b: "{{ result | json_query(controller_b_query) }}"
|
||||||
|
vars:
|
||||||
|
controller_b_query: "json[?controllerId=='{{ controller_b_id }}']"
|
||||||
|
|
||||||
|
# Confirm that all of controller B's ports are disabled
|
||||||
|
- name: Verify all controller B ports are disabled
|
||||||
|
assert:
|
||||||
|
that: "{{ item.ipv4Enabled == false }}"
|
||||||
|
msg: "Controller B, channel {{ item.channel }} is not disabled"
|
||||||
|
loop: "{{ controller_b }}"
|
|
@ -0,0 +1,255 @@
|
||||||
|
# (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 ansible.modules.storage.netapp.netapp_e_iscsi_interface import IscsiInterface
|
||||||
|
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 IscsiInterfaceTest(ModuleTestCase):
|
||||||
|
REQUIRED_PARAMS = {
|
||||||
|
'api_username': 'rw',
|
||||||
|
'api_password': 'password',
|
||||||
|
'api_url': 'http://localhost',
|
||||||
|
'ssid': '1',
|
||||||
|
'state': 'disabled',
|
||||||
|
'name': 1,
|
||||||
|
'controller': 'A',
|
||||||
|
}
|
||||||
|
REQ_FUNC = 'ansible.modules.storage.netapp.netapp_e_iscsi_interface.request'
|
||||||
|
|
||||||
|
def _set_args(self, args=None):
|
||||||
|
module_args = self.REQUIRED_PARAMS.copy()
|
||||||
|
if args is not None:
|
||||||
|
module_args.update(args)
|
||||||
|
set_module_args(module_args)
|
||||||
|
|
||||||
|
def test_validate_params(self):
|
||||||
|
"""Ensure we can pass valid parameters to the module"""
|
||||||
|
# Provide a range of valid values for each
|
||||||
|
for controller in ['A', 'B']:
|
||||||
|
for i in range(1, 10):
|
||||||
|
for mtu in [1500, 2500, 9000]:
|
||||||
|
self._set_args(dict(
|
||||||
|
state='disabled',
|
||||||
|
name=i,
|
||||||
|
controller=controller,
|
||||||
|
mtu=mtu,
|
||||||
|
))
|
||||||
|
iface = IscsiInterface()
|
||||||
|
|
||||||
|
def test_invalid_params(self):
|
||||||
|
"""Ensure that our input validation catches invalid parameters"""
|
||||||
|
|
||||||
|
# Currently a 'C' controller is invalid
|
||||||
|
self._set_args(dict(
|
||||||
|
state='disabled',
|
||||||
|
name=1,
|
||||||
|
controller="C",
|
||||||
|
))
|
||||||
|
with self.assertRaises(AnsibleFailJson) as result:
|
||||||
|
iface = IscsiInterface()
|
||||||
|
|
||||||
|
# Each of these mtu values are invalid
|
||||||
|
for mtu in [500, 1499, 9001]:
|
||||||
|
self._set_args({
|
||||||
|
'state': 'disabled',
|
||||||
|
'name': 1,
|
||||||
|
'controller': 'A',
|
||||||
|
'mtu': mtu
|
||||||
|
})
|
||||||
|
with self.assertRaises(AnsibleFailJson) as result:
|
||||||
|
iface = IscsiInterface()
|
||||||
|
|
||||||
|
def test_interfaces(self):
|
||||||
|
"""Validate that we are processing the interface list properly"""
|
||||||
|
self._set_args()
|
||||||
|
|
||||||
|
interfaces = [
|
||||||
|
dict(interfaceType='iscsi',
|
||||||
|
iscsi=dict()),
|
||||||
|
dict(interfaceType='iscsi',
|
||||||
|
iscsi=dict()),
|
||||||
|
dict(interfaceType='fc', )
|
||||||
|
]
|
||||||
|
|
||||||
|
# Ensure we filter out anything without an interfaceType of iscsi
|
||||||
|
expected = [iface['iscsi'] for iface in interfaces if iface['interfaceType'] == 'iscsi']
|
||||||
|
|
||||||
|
# We expect a single call to the API: retrieve the list of interfaces from the objectGraph.
|
||||||
|
with mock.patch(self.REQ_FUNC, return_value=(200, interfaces)):
|
||||||
|
iface = IscsiInterface()
|
||||||
|
interfaces = iface.interfaces
|
||||||
|
self.assertEquals(interfaces, expected)
|
||||||
|
|
||||||
|
def test_interfaces_fail(self):
|
||||||
|
"""Ensure we fail gracefully on an error to retrieve the interfaces"""
|
||||||
|
self._set_args()
|
||||||
|
|
||||||
|
with self.assertRaises(AnsibleFailJson) as result:
|
||||||
|
# Simulate a failed call to the API
|
||||||
|
with mock.patch(self.REQ_FUNC, side_effect=Exception("Failure")):
|
||||||
|
iface = IscsiInterface()
|
||||||
|
interfaces = iface.interfaces
|
||||||
|
|
||||||
|
def test_fetch_target_interface_bad_channel(self):
|
||||||
|
"""Ensure we fail correctly when a bad channel is provided"""
|
||||||
|
self._set_args()
|
||||||
|
|
||||||
|
interfaces = list(dict(channel=1, controllerId='1'))
|
||||||
|
|
||||||
|
with self.assertRaisesRegexp(AnsibleFailJson, r".*?channels include.*"):
|
||||||
|
with mock.patch.object(IscsiInterface, 'interfaces', return_value=interfaces):
|
||||||
|
iface = IscsiInterface()
|
||||||
|
interfaces = iface.fetch_target_interface()
|
||||||
|
|
||||||
|
def test_make_update_body_dhcp(self):
|
||||||
|
"""Ensure the update body generates correctly for a transition from static to dhcp"""
|
||||||
|
self._set_args(dict(state='enabled',
|
||||||
|
config_method='dhcp')
|
||||||
|
)
|
||||||
|
|
||||||
|
iface = dict(id='1',
|
||||||
|
ipv4Enabled=False,
|
||||||
|
ipv4Data=dict(ipv4AddressData=dict(ipv4Address="0.0.0.0",
|
||||||
|
ipv4SubnetMask="0.0.0.0",
|
||||||
|
ipv4GatewayAddress="0.0.0.0", ),
|
||||||
|
ipv4AddressConfigMethod='configStatic', ),
|
||||||
|
interfaceData=dict(ethernetData=dict(maximumFramePayloadSize=1500, ), ),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test a transition from static to dhcp
|
||||||
|
inst = IscsiInterface()
|
||||||
|
update, body = inst.make_update_body(iface)
|
||||||
|
self.assertTrue(update, msg="An update was expected!")
|
||||||
|
self.assertEquals(body['settings']['ipv4Enabled'][0], True)
|
||||||
|
self.assertEquals(body['settings']['ipv4AddressConfigMethod'][0], 'configDhcp')
|
||||||
|
|
||||||
|
def test_make_update_body_static(self):
|
||||||
|
"""Ensure the update body generates correctly for a transition from dhcp to static"""
|
||||||
|
iface = dict(id='1',
|
||||||
|
ipv4Enabled=False,
|
||||||
|
ipv4Data=dict(ipv4AddressConfigMethod='configDhcp',
|
||||||
|
ipv4AddressData=dict(ipv4Address="0.0.0.0",
|
||||||
|
ipv4SubnetMask="0.0.0.0",
|
||||||
|
ipv4GatewayAddress="0.0.0.0", ), ),
|
||||||
|
interfaceData=dict(ethernetData=dict(maximumFramePayloadSize=1500, ), ), )
|
||||||
|
|
||||||
|
self._set_args(dict(state='enabled',
|
||||||
|
config_method='static',
|
||||||
|
address='10.10.10.10',
|
||||||
|
subnet_mask='255.255.255.0',
|
||||||
|
gateway='1.1.1.1'))
|
||||||
|
|
||||||
|
inst = IscsiInterface()
|
||||||
|
update, body = inst.make_update_body(iface)
|
||||||
|
self.assertTrue(update, msg="An update was expected!")
|
||||||
|
self.assertEquals(body['settings']['ipv4Enabled'][0], True)
|
||||||
|
self.assertEquals(body['settings']['ipv4AddressConfigMethod'][0], 'configStatic')
|
||||||
|
self.assertEquals(body['settings']['ipv4Address'][0], '10.10.10.10')
|
||||||
|
self.assertEquals(body['settings']['ipv4SubnetMask'][0], '255.255.255.0')
|
||||||
|
self.assertEquals(body['settings']['ipv4GatewayAddress'][0], '1.1.1.1')
|
||||||
|
|
||||||
|
CONTROLLERS = dict(A='1', B='2')
|
||||||
|
|
||||||
|
def test_update_bad_controller(self):
|
||||||
|
"""Ensure a bad controller fails gracefully"""
|
||||||
|
self._set_args(dict(controller='B'))
|
||||||
|
|
||||||
|
inst = IscsiInterface()
|
||||||
|
with self.assertRaises(AnsibleFailJson) as result:
|
||||||
|
with mock.patch.object(inst, 'get_controllers', return_value=dict(A='1')) as get_controllers:
|
||||||
|
inst()
|
||||||
|
|
||||||
|
@mock.patch.object(IscsiInterface, 'get_controllers', return_value=CONTROLLERS)
|
||||||
|
def test_update(self, get_controllers):
|
||||||
|
"""Validate the good path"""
|
||||||
|
self._set_args()
|
||||||
|
|
||||||
|
inst = IscsiInterface()
|
||||||
|
with self.assertRaises(AnsibleExitJson):
|
||||||
|
with mock.patch(self.REQ_FUNC, return_value=(200, "")) as request:
|
||||||
|
with mock.patch.object(inst, 'fetch_target_interface', side_effect=[{}, mock.MagicMock()]):
|
||||||
|
with mock.patch.object(inst, 'make_update_body', return_value=(True, {})):
|
||||||
|
inst()
|
||||||
|
request.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch.object(IscsiInterface, 'get_controllers', return_value=CONTROLLERS)
|
||||||
|
def test_update_not_required(self, get_controllers):
|
||||||
|
"""Ensure we don't trigger the update if one isn't required or if check mode is enabled"""
|
||||||
|
self._set_args()
|
||||||
|
|
||||||
|
# make_update_body will report that no change is required, so we should see no call to the API.
|
||||||
|
inst = IscsiInterface()
|
||||||
|
with self.assertRaises(AnsibleExitJson) as result:
|
||||||
|
with mock.patch(self.REQ_FUNC, return_value=(200, "")) as request:
|
||||||
|
with mock.patch.object(inst, 'fetch_target_interface', side_effect=[{}, mock.MagicMock()]):
|
||||||
|
with mock.patch.object(inst, 'make_update_body', return_value=(False, {})):
|
||||||
|
inst()
|
||||||
|
request.assert_not_called()
|
||||||
|
self.assertFalse(result.exception.args[0]['changed'], msg="No change was expected.")
|
||||||
|
|
||||||
|
# Since check_mode is enabled, we will run everything normally, but not make a request to the API
|
||||||
|
# to perform the actual change.
|
||||||
|
inst = IscsiInterface()
|
||||||
|
inst.check_mode = True
|
||||||
|
with self.assertRaises(AnsibleExitJson) as result:
|
||||||
|
with mock.patch(self.REQ_FUNC, return_value=(200, "")) as request:
|
||||||
|
with mock.patch.object(inst, 'fetch_target_interface', side_effect=[{}, mock.MagicMock()]):
|
||||||
|
with mock.patch.object(inst, 'make_update_body', return_value=(True, {})):
|
||||||
|
inst()
|
||||||
|
request.assert_not_called()
|
||||||
|
self.assertTrue(result.exception.args[0]['changed'], msg="A change was expected.")
|
||||||
|
|
||||||
|
@mock.patch.object(IscsiInterface, 'get_controllers', return_value=CONTROLLERS)
|
||||||
|
def test_update_fail_busy(self, get_controllers):
|
||||||
|
"""Ensure we fail correctly on receiving a busy response from the API."""
|
||||||
|
self._set_args()
|
||||||
|
|
||||||
|
inst = IscsiInterface()
|
||||||
|
with self.assertRaisesRegexp(AnsibleFailJson, r".*?busy.*") as result:
|
||||||
|
with mock.patch(self.REQ_FUNC, return_value=(422, dict(retcode="3"))) as request:
|
||||||
|
with mock.patch.object(inst, 'fetch_target_interface', side_effect=[{}, mock.MagicMock()]):
|
||||||
|
with mock.patch.object(inst, 'make_update_body', return_value=(True, {})):
|
||||||
|
inst()
|
||||||
|
request.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch.object(IscsiInterface, 'get_controllers', return_value=CONTROLLERS)
|
||||||
|
@mock.patch.object(IscsiInterface, 'make_update_body', return_value=(True, {}))
|
||||||
|
def test_update_fail(self, get_controllers, make_body):
|
||||||
|
"""Ensure we fail correctly on receiving a normal failure from the API."""
|
||||||
|
self._set_args()
|
||||||
|
|
||||||
|
inst = IscsiInterface()
|
||||||
|
# Test a 422 error with a non-busy status
|
||||||
|
with self.assertRaisesRegexp(AnsibleFailJson, r".*?Failed to modify.*") as result:
|
||||||
|
with mock.patch(self.REQ_FUNC, return_value=(422, mock.MagicMock())) as request:
|
||||||
|
with mock.patch.object(inst, 'fetch_target_interface', side_effect=[{}, mock.MagicMock()]):
|
||||||
|
inst()
|
||||||
|
request.assert_called_once()
|
||||||
|
|
||||||
|
# Test a 401 (authentication) error
|
||||||
|
with self.assertRaisesRegexp(AnsibleFailJson, r".*?Failed to modify.*") as result:
|
||||||
|
with mock.patch(self.REQ_FUNC, return_value=(401, mock.MagicMock())) as request:
|
||||||
|
with mock.patch.object(inst, 'fetch_target_interface', side_effect=[{}, mock.MagicMock()]):
|
||||||
|
inst()
|
||||||
|
request.assert_called_once()
|
||||||
|
|
||||||
|
# Test with a connection failure
|
||||||
|
with self.assertRaisesRegexp(AnsibleFailJson, r".*?Connection failure.*") as result:
|
||||||
|
with mock.patch(self.REQ_FUNC, side_effect=Exception()) as request:
|
||||||
|
with mock.patch.object(inst, 'fetch_target_interface', side_effect=[{}, mock.MagicMock()]):
|
||||||
|
inst()
|
||||||
|
request.assert_called_once()
|
Loading…
Reference in a new issue