diff --git a/lib/ansible/modules/storage/netapp/netapp_e_mgmt_interface.py b/lib/ansible/modules/storage/netapp/netapp_e_mgmt_interface.py new file mode 100644 index 0000000000..98e4029f17 --- /dev/null +++ b/lib/ansible/modules/storage/netapp/netapp_e_mgmt_interface.py @@ -0,0 +1,708 @@ +#!/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_mgmt_interface +short_description: NetApp E-Series management interface configuration +description: + - Configure the E-Series management interfaces +version_added: '2.7' +author: + - Michael Price (@lmprice) + - Nathan Swartz (@ndswartz) +extends_documentation_fragment: + - netapp.eseries +options: + state: + description: + - Enable or disable IPv4 network interface configuration. + - Either IPv4 or IPv6 must be enabled otherwise error will occur. + - Only required when enabling or disabling IPv4 network interface + choices: + - enable + - disable + required: no + aliases: + - enable_interface + controller: + description: + - The controller that owns the port you want to configure. + - Controller names are represented 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 port to modify the configuration for. + - The list of choices is not necessarily comprehensive. It depends on the number of ports + that are present in the system. + - The name represents the port number (typically from left to right on the controller), + beginning with a value of 1. + - Mutually exclusive with I(channel). + aliases: + - port + - iface + channel: + description: + - The port to modify the configuration for. + - The channel represents the port number (typically from left to right on the controller), + beginning with a value of 1. + - Mutually exclusive with I(name). + 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) + required: no + 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) + required: no + 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) + required: no + config_method: + description: + - The configuration method type to use for network interface ports. + - dhcp is mutually exclusive with I(address), I(subnet_mask), and I(gateway). + choices: + - dhcp + - static + required: no + dns_config_method: + description: + - The configuration method type to use for DNS services. + - dhcp is mutually exclusive with I(dns_address), and I(dns_address_backup). + choices: + - dhcp + - static + required: no + dns_address: + description: + - Primary IPv4 DNS server address + required: no + dns_address_backup: + description: + - Backup IPv4 DNS server address + - Queried when primary DNS server fails + required: no + ntp_config_method: + description: + - The configuration method type to use for NTP services. + - disable is mutually exclusive with I(ntp_address) and I(ntp_address_backup). + - dhcp is mutually exclusive with I(ntp_address) and I(ntp_address_backup). + choices: + - disable + - dhcp + - static + required: no + ntp_address: + description: + - Primary IPv4 NTP server address + required: no + ntp_address_backup: + description: + - Backup IPv4 NTP server address + - Queried when primary NTP server fails + required: no + ssh: + type: bool + description: + - Enable ssh access to the controller for debug purposes. + - This is a controller-level setting. + - rlogin/telnet will be enabled for ancient equipment where ssh is not available. + required: no + 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. + - "Known issue: Changes specifically to down ports will result in a failure. However, this may not be the case in up + coming NetApp E-Series firmware releases (released after firmware version 11.40.2)." +""" + +EXAMPLES = """ + - name: Configure the first port on the A controller with a static IPv4 address + netapp_e_mgmt_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_mgmt_interface: + name: "2" + controller: "B" + enable_interface: no + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + + - name: Enable ssh access for ports one and two on controller A + netapp_e_mgmt_interface: + name: "{{ item }}" + controller: "A" + ssh: yes + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + loop: + - 1 + - 2 + + - name: Configure static DNS settings for the first port on controller A + netapp_e_mgmt_interface: + name: "1" + controller: "A" + dns_config_method: static + dns_address: "192.168.1.100" + dns_address_backup: "192.168.1.1" + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + + - name: Configure static NTP settings for ports one and two on controller B + netapp_e_mgmt_interface: + name: "{{ item }}" + controller: "B" + ntp_config_method: static + ntp_address: "129.100.1.100" + ntp_address_backup: "127.100.1.1" + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + loop: + - 1 + - 2 +""" + +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 absent 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, pprint +import time +import socket + +try: + import urlparse +except ImportError: + import urllib.parse as urlparse + +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 MgmtInterface(object): + MAX_RETRIES = 15 + + def __init__(self): + argument_spec = eseries_host_argument_spec() + argument_spec.update(dict( + state=dict(type="str", choices=["enable", "disable"], + aliases=["enable_interface"], required=False), + controller=dict(type="str", required=True, choices=["A", "B"]), + name=dict(type="str", aliases=["port", "iface"]), + channel=dict(type="int"), + 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, choices=["dhcp", "static"]), + dns_config_method=dict(type="str", required=False, choices=["dhcp", "static"]), + dns_address=dict(type="str", required=False), + dns_address_backup=dict(type="str", required=False), + ntp_config_method=dict(type="str", required=False, choices=["disable", "dhcp", "static"]), + ntp_address=dict(type="str", required=False), + ntp_address_backup=dict(type="str", required=False), + ssh=dict(type="bool", required=False), + log_path=dict(type="str", required=False), + )) + + required_if = [ + ["state", "enable", ["config_method"]], + ["config_method", "static", ["address", "subnet_mask"]], + ["dns_config_method", "static", ["dns_address"]], + ["ntp_config_method", "static", ["ntp_address"]], + ] + + mutually_exclusive = [ + ["name", "channel"], + ] + + self.module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True, + required_if=required_if, + mutually_exclusive=mutually_exclusive) + args = self.module.params + + self.controller = args["controller"] + self.name = args["name"] + self.channel = args["channel"] + + self.config_method = args["config_method"] + self.address = args["address"] + self.subnet_mask = args["subnet_mask"] + self.gateway = args["gateway"] + self.enable_interface = None if args["state"] is None else args["state"] == "enable" + + self.dns_config_method = args["dns_config_method"] + self.dns_address = args["dns_address"] + self.dns_address_backup = args["dns_address_backup"] + + self.ntp_config_method = args["ntp_config_method"] + self.ntp_address = args["ntp_address"] + self.ntp_address_backup = args["ntp_address_backup"] + + self.ssh = args["ssh"] + + 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.retries = 0 + + self.check_mode = self.module.check_mode + self.post_body = dict() + + 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 += '/' + + @property + def controllers(self): + """Retrieve a mapping of controller labels to their references + { + 'A': '070000000000000000000001', + 'B': '070000000000000000000002', + } + :return: the controllers defined on the system + """ + try: + (rc, controllers) = request(self.url + 'storage-systems/%s/controllers' + % self.ssid, headers=HEADERS, **self.creds) + except Exception as err: + controllers = list() + self.module.fail_json( + msg="Failed to retrieve the controller settings. Array Id [%s]. Error [%s]." + % (self.ssid, to_native(err))) + + controllers.sort(key=lambda c: c['physicalLocation']['slot']) + + controllers_dict = dict() + i = ord('A') + for controller in controllers: + label = chr(i) + settings = dict(controllerSlot=controller['physicalLocation']['slot'], + controllerRef=controller['controllerRef'], + ssh=controller['networkSettings']['remoteAccessEnabled']) + controllers_dict[label] = settings + i += 1 + + return controllers_dict + + @property + def interface(self): + net_interfaces = list() + try: + (rc, net_interfaces) = request(self.url + 'storage-systems/%s/configuration/ethernet-interfaces' + % self.ssid, headers=HEADERS, **self.creds) + except Exception as err: + self.module.fail_json( + msg="Failed to retrieve defined management interfaces. Array Id [%s]. Error [%s]." + % (self.ssid, to_native(err))) + + controllers = self.controllers + controller = controllers[self.controller] + + net_interfaces = [iface for iface in net_interfaces if iface["controllerRef"] == controller["controllerRef"]] + + # Find the correct interface + iface = None + for net in net_interfaces: + if self.name: + if net["alias"] == self.name or net["interfaceName"] == self.name: + iface = net + break + elif self.channel: + if net["channel"] == self.channel: + iface = net + break + + if iface is None: + identifier = self.name if self.name is not None else self.channel + self.module.fail_json(msg="We could not find an interface matching [%s] on Array=[%s]." + % (identifier, self.ssid)) + + return dict(alias=iface["alias"], + channel=iface["channel"], + link_status=iface["linkStatus"], + enabled=iface["ipv4Enabled"], + address=iface["ipv4Address"], + gateway=iface["ipv4GatewayAddress"], + subnet_mask=iface["ipv4SubnetMask"], + dns_config_method=iface["dnsProperties"]["acquisitionProperties"]["dnsAcquisitionType"], + dns_servers=iface["dnsProperties"]["acquisitionProperties"]["dnsServers"], + ntp_config_method=iface["ntpProperties"]["acquisitionProperties"]["ntpAcquisitionType"], + ntp_servers=iface["ntpProperties"]["acquisitionProperties"]["ntpServers"], + config_method=iface["ipv4AddressConfigMethod"], + controllerRef=iface["controllerRef"], + controllerSlot=iface["controllerSlot"], + ipv6Enabled=iface["ipv6Enabled"], + id=iface["interfaceRef"], ) + + def get_enable_interface_settings(self, iface, expected_iface, update, body): + """Enable or disable the IPv4 network interface.""" + if self.enable_interface: + if not iface["enabled"]: + update = True + body["ipv4Enabled"] = True + else: + if iface["enabled"]: + update = True + body["ipv4Enabled"] = False + + expected_iface["enabled"] = body["ipv4Enabled"] + return update, expected_iface, body + + def get_interface_settings(self, iface, expected_iface, update, body): + """Update network interface settings.""" + + if self.config_method == "dhcp": + if iface["config_method"] != "configDhcp": + update = True + body["ipv4AddressConfigMethod"] = "configDhcp" + + else: + if iface["config_method"] != "configStatic": + update = True + body["ipv4AddressConfigMethod"] = "configStatic" + + if iface["address"] != self.address: + update = True + body["ipv4Address"] = self.address + + if iface["subnet_mask"] != self.subnet_mask: + update = True + body["ipv4SubnetMask"] = self.subnet_mask + + if self.gateway and iface["gateway"] != self.gateway: + update = True + body["ipv4GatewayAddress"] = self.gateway + + expected_iface["address"] = body["ipv4Address"] + expected_iface["subnet_mask"] = body["ipv4SubnetMask"] + expected_iface["gateway"] = body["ipv4GatewayAddress"] + + expected_iface["config_method"] = body["ipv4AddressConfigMethod"] + + return update, expected_iface, body + + def get_dns_server_settings(self, iface, expected_iface, update, body): + """Add DNS server information to the request body.""" + if self.dns_config_method == "dhcp": + if iface["dns_config_method"] != "dhcp": + update = True + body["dnsAcquisitionDescriptor"] = dict(dnsAcquisitionType="dhcp") + + elif self.dns_config_method == "static": + dns_servers = [dict(addressType="ipv4", ipv4Address=self.dns_address)] + if self.dns_address_backup: + dns_servers.append(dict(addressType="ipv4", ipv4Address=self.dns_address_backup)) + + body["dnsAcquisitionDescriptor"] = dict(dnsAcquisitionType="stat", dnsServers=dns_servers) + + if (iface["dns_config_method"] != "stat" or + len(iface["dns_servers"]) != len(dns_servers) or + (len(iface["dns_servers"]) == 2 and + (iface["dns_servers"][0]["ipv4Address"] != self.dns_address or + iface["dns_servers"][1]["ipv4Address"] != self.dns_address_backup)) or + (len(iface["dns_servers"]) == 1 and + iface["dns_servers"][0]["ipv4Address"] != self.dns_address)): + update = True + + expected_iface["dns_servers"] = dns_servers + + expected_iface["dns_config_method"] = body["dnsAcquisitionDescriptor"]["dnsAcquisitionType"] + return update, expected_iface, body + + def get_ntp_server_settings(self, iface, expected_iface, update, body): + """Add NTP server information to the request body.""" + if self.ntp_config_method == "disable": + if iface["ntp_config_method"] != "disabled": + update = True + body["ntpAcquisitionDescriptor"] = dict(ntpAcquisitionType="disabled") + + elif self.ntp_config_method == "dhcp": + if iface["ntp_config_method"] != "dhcp": + update = True + body["ntpAcquisitionDescriptor"] = dict(ntpAcquisitionType="dhcp") + + elif self.ntp_config_method == "static": + ntp_servers = [dict(addrType="ipvx", ipvxAddress=dict(addressType="ipv4", ipv4Address=self.ntp_address))] + if self.ntp_address_backup: + ntp_servers.append(dict(addrType="ipvx", + ipvxAddress=dict(addressType="ipv4", ipv4Address=self.ntp_address_backup))) + + body["ntpAcquisitionDescriptor"] = dict(ntpAcquisitionType="stat", ntpServers=ntp_servers) + + if (iface["ntp_config_method"] != "stat" or + len(iface["ntp_servers"]) != len(ntp_servers) or + ((len(iface["ntp_servers"]) == 2 and + (iface["ntp_servers"][0]["ipvxAddress"]["ipv4Address"] != self.ntp_address or + iface["ntp_servers"][1]["ipvxAddress"]["ipv4Address"] != self.ntp_address_backup)) or + (len(iface["ntp_servers"]) == 1 and + iface["ntp_servers"][0]["ipvxAddress"]["ipv4Address"] != self.ntp_address))): + update = True + + expected_iface["ntp_servers"] = ntp_servers + + expected_iface["ntp_config_method"] = body["ntpAcquisitionDescriptor"]["ntpAcquisitionType"] + return update, expected_iface, body + + def get_remote_ssh_settings(self, settings, update, body): + """Configure network interface ports for remote ssh access.""" + if self.ssh != settings["ssh"]: + update = True + + body["enableRemoteAccess"] = self.ssh + return update, body + + def update_array(self, settings, iface): + """Update controller with new interface, dns service, ntp service and/or remote ssh access information. + + :returns: whether information passed will modify the controller's current state + :rtype: bool + """ + update = False + body = dict(controllerRef=settings['controllerRef'], + interfaceRef=iface['id']) + expected_iface = iface.copy() + + # Check if api url is using the effected management interface to change itself + update_used_matching_address = False + if self.enable_interface and self.config_method: + netloc = list(urlparse.urlparse(self.url))[1] + address = netloc.split(":")[0] + address_info = socket.getaddrinfo(address, 8443) + url_address_info = socket.getaddrinfo(iface["address"], 8443) + update_used_matching_address = any(info in url_address_info for info in address_info) + + self._logger.info("update_used_matching_address: " + str(update_used_matching_address)) + + # Populate the body of the request and check for changes + if self.enable_interface is not None: + update, expected_iface, body = self.get_enable_interface_settings(iface, expected_iface, update, body) + + if self.config_method is not None: + update, expected_iface, body = self.get_interface_settings(iface, expected_iface, update, body) + + if self.dns_config_method is not None: + update, expected_iface, body = self.get_dns_server_settings(iface, expected_iface, update, body) + + if self.ntp_config_method is not None: + update, expected_iface, body = self.get_ntp_server_settings(iface, expected_iface, update, body) + + if self.ssh is not None: + update, body = self.get_remote_ssh_settings(settings, update, body) + iface["ssh"] = self.ssh + expected_iface["ssh"] = self.ssh + + # debug information + self._logger.info(pformat(body)) + self._logger.info(pformat(iface)) + self._logger.info(pformat(expected_iface)) + + if self.check_mode: + return update + + if update and not self.check_mode: + if not update_used_matching_address: + try: + (rc, data) = request(self.url + 'storage-systems/%s/configuration/ethernet-interfaces' + % self.ssid, method='POST', data=json.dumps(body), headers=HEADERS, + timeout=300, ignore_errors=True, **self.creds) + if rc == 422: + if data['retcode'] == "4" or data['retcode'] == "illegalParam": + if not (body['ipv4Enabled'] or iface['ipv6Enabled']): + self.module.fail_json(msg="This storage-system already has IPv6 connectivity disabled. " + "DHCP configuration for IPv4 is required at a minimum." + " Array Id [%s] Message [%s]." + % (self.ssid, data['errorMessage'])) + else: + self.module.fail_json(msg="We failed to configure the management interface. Array Id " + "[%s] Message [%s]." % (self.ssid, data)) + elif rc >= 300: + self.module.fail_json( + msg="We failed to configure the management interface. Array Id [%s] Message [%s]." % + (self.ssid, data)) + + # 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 network settings! Array Id [%s]. Error [%s]." + % (self.ssid, to_native(err))) + else: + self.update_api_address_interface_match(body) + + return self.validate_changes(expected_iface) if update and iface["link_status"] != "up" else update + + def update_api_address_interface_match(self, body): + """Change network interface address which matches the api_address""" + try: + try: + (rc, data) = request(self.url + 'storage-systems/%s/configuration/ethernet-interfaces' % self.ssid, + use_proxy=False, force=True, ignore_errors=True, method='POST', + data=json.dumps(body), headers=HEADERS, timeout=10, **self.creds) + except Exception: + url_parts = list(urlparse.urlparse(self.url)) + domain = url_parts[1].split(":") + domain[0] = self.address + url_parts[1] = ":".join(domain) + expected_url = urlparse.urlunparse(url_parts) + self._logger.info(pformat(expected_url)) + + (rc, data) = request(expected_url + 'storage-systems/%s/configuration/ethernet-interfaces' % self.ssid, + headers=HEADERS, timeout=300, **self.creds) + return + except Exception as err: + self._logger.info(type(err)) + self.module.fail_json( + msg="Connection failure: we failed to modify the network settings! Array Id [%s]. Error [%s]." + % (self.ssid, to_native(err))) + + def validate_changes(self, expected_iface, retry=6): + """Validate interface changes were applied to the controller interface port. 30 second timeout""" + if self.interface != expected_iface: + time.sleep(5) + if retry: + return self.validate_changes(expected_iface, retry - 1) + + self.module.fail_json(msg="Update failure: we failed to verify the necessary state change.") + + return True + + def check_health(self): + """It's possible, due to a previous operation, for the API to report a 424 (offline) status for the + storage-system. Therefore, we run a manual check with retries to attempt to contact the system before we + continue. + """ + try: + (rc, data) = request(self.url + 'storage-systems/%s/controllers' + % self.ssid, headers=HEADERS, + ignore_errors=True, **self.creds) + + # We've probably recently changed the interface settings and it's still coming back up: retry. + if rc == 424: + if self.retries < self.MAX_RETRIES: + self.retries += 1 + self._logger.info("We hit a 424, retrying in 5s.") + time.sleep(5) + self.check_health() + else: + self.module.fail_json( + msg="We failed to pull storage-system information. Array Id [%s] Message [%s]." % + (self.ssid, data)) + elif rc >= 300: + self.module.fail_json( + msg="We failed to pull storage-system information. Array Id [%s] Message [%s]." % + (self.ssid, data)) + # This is going to catch cases like a connection failure + except Exception as err: + if self.retries < self.MAX_RETRIES: + self._logger.info("We hit a connection failure, retrying in 5s.") + self.retries += 1 + time.sleep(5) + self.check_health() + else: + self.module.fail_json( + msg="Connection failure: we failed to modify the network settings! Array Id [%s]. Error [%s]." + % (self.ssid, to_native(err))) + + def update(self): + """Update storage system with necessary changes.""" + # Check if the storage array can be contacted + self.check_health() + + # make the necessary changes to the storage system + settings = self.controllers[self.controller] + iface = self.interface + self._logger.info(pformat(settings)) + self._logger.info(pformat(iface)) + update = self.update_array(settings, iface) + + self.module.exit_json(msg="The interface settings have been updated.", changed=update) + + def __call__(self, *args, **kwargs): + self.update() + + +def main(): + iface = MgmtInterface() + iface() + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/storage/netapp/test_netapp_e_mgmt_interface.py b/test/units/modules/storage/netapp/test_netapp_e_mgmt_interface.py new file mode 100644 index 0000000000..cf1de64277 --- /dev/null +++ b/test/units/modules/storage/netapp/test_netapp_e_mgmt_interface.py @@ -0,0 +1,684 @@ +# coding=utf-8 +# (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.modules.storage.netapp.netapp_e_mgmt_interface import MgmtInterface +from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args + +__metaclass__ = type + +import mock +from ansible.compat.tests.mock import PropertyMock + + +class MgmtInterfaceTest(ModuleTestCase): + REQUIRED_PARAMS = { + 'api_username': 'rw', + 'api_password': 'password', + 'api_url': 'http://localhost', + 'ssid': '1', + } + + TEST_DATA = [ + { + "controllerRef": "070000000000000000000001", + "controllerSlot": 1, + "interfaceName": "wan0", + "interfaceRef": "2800070000000000000000000001000000000000", + "channel": 1, + "alias": "creG1g-AP-a", + "ipv4Enabled": True, + "ipv4Address": "10.1.1.10", + "linkStatus": "up", + "ipv4SubnetMask": "255.255.255.0", + "ipv4AddressConfigMethod": "configStatic", + "ipv4GatewayAddress": "10.1.1.1", + "ipv6Enabled": False, + "physicalLocation": { + "slot": 0, + }, + "dnsProperties": { + "acquisitionProperties": { + "dnsAcquisitionType": "stat", + "dnsServers": [ + { + "addressType": "ipv4", + "ipv4Address": "10.1.0.250", + }, + { + "addressType": "ipv4", + "ipv4Address": "10.10.0.20", + } + ] + }, + "dhcpAcquiredDnsServers": [] + }, + "ntpProperties": { + "acquisitionProperties": { + "ntpAcquisitionType": "disabled", + "ntpServers": None + }, + "dhcpAcquiredNtpServers": [] + }, + }, + { + "controllerRef": "070000000000000000000001", + "controllerSlot": 1, + "interfaceName": "wan1", + "interfaceRef": "2800070000000000000000000001000000000000", + "channel": 2, + "alias": "creG1g-AP-a", + "ipv4Enabled": True, + "ipv4Address": "0.0.0.0", + "ipv4SubnetMask": "0.0.0.0", + "ipv4AddressConfigMethod": "configDhcp", + "ipv4GatewayAddress": "10.1.1.1", + "ipv6Enabled": False, + "physicalLocation": { + "slot": 1, + }, + "dnsProperties": { + "acquisitionProperties": { + "dnsAcquisitionType": "stat", + "dnsServers": [ + { + "addressType": "ipv4", + "ipv4Address": "10.1.0.250", + "ipv6Address": None + }, + { + "addressType": "ipv4", + "ipv4Address": "10.10.0.20", + "ipv6Address": None + } + ] + }, + "dhcpAcquiredDnsServers": [] + }, + "ntpProperties": { + "acquisitionProperties": { + "ntpAcquisitionType": "disabled", + "ntpServers": None + }, + "dhcpAcquiredNtpServers": [] + }, + }, + { + "controllerRef": "070000000000000000000002", + "controllerSlot": 2, + "interfaceName": "wan0", + "interfaceRef": "2800070000000000000000000001000000000000", + "channel": 1, + "alias": "creG1g-AP-b", + "ipv4Enabled": True, + "ipv4Address": "0.0.0.0", + "ipv4SubnetMask": "0.0.0.0", + "ipv4AddressConfigMethod": "configDhcp", + "ipv4GatewayAddress": "10.1.1.1", + "ipv6Enabled": False, + "physicalLocation": { + "slot": 0, + }, + "dnsProperties": { + "acquisitionProperties": { + "dnsAcquisitionType": "stat", + "dnsServers": [ + { + "addressType": "ipv4", + "ipv4Address": "10.1.0.250", + "ipv6Address": None + } + ] + }, + "dhcpAcquiredDnsServers": [] + }, + "ntpProperties": { + "acquisitionProperties": { + "ntpAcquisitionType": "stat", + "ntpServers": [ + { + "addrType": "ipvx", + "domainName": None, + "ipvxAddress": { + "addressType": "ipv4", + "ipv4Address": "10.13.1.5", + "ipv6Address": None + } + }, + { + "addrType": "ipvx", + "domainName": None, + "ipvxAddress": { + "addressType": "ipv4", + "ipv4Address": "10.15.1.8", + "ipv6Address": None + } + } + ] + }, + "dhcpAcquiredNtpServers": [] + }, + }, + { + "controllerRef": "070000000000000000000002", + "controllerSlot": 2, + "interfaceName": "wan1", + "interfaceRef": "2801070000000000000000000001000000000000", + "channel": 2, + "alias": "creG1g-AP-b", + "ipv4Enabled": True, + "ipv4Address": "0.0.0.0", + "ipv4SubnetMask": "0.0.0.0", + "ipv4AddressConfigMethod": "configDhcp", + "ipv4GatewayAddress": "10.1.1.1", + "ipv6Enabled": False, + "physicalLocation": { + "slot": 1, + }, + "dnsProperties": { + "acquisitionProperties": { + "dnsAcquisitionType": "stat", + "dnsServers": [ + { + "addressType": "ipv4", + "ipv4Address": "10.19.1.2", + "ipv6Address": None + } + ] + }, + "dhcpAcquiredDnsServers": [] + }, + "ntpProperties": { + "acquisitionProperties": { + "ntpAcquisitionType": "stat", + "ntpServers": [ + { + "addrType": "ipvx", + "domainName": None, + "ipvxAddress": { + "addressType": "ipv4", + "ipv4Address": "10.13.1.5", + "ipv6Address": None + } + }, + { + "addrType": "ipvx", + "domainName": None, + "ipvxAddress": { + "addressType": "ipv4", + "ipv4Address": "10.15.1.18", + "ipv6Address": None + } + } + ] + }, + "dhcpAcquiredNtpServers": [] + }, + }, + ] + + REQ_FUNC = 'ansible.modules.storage.netapp.netapp_e_mgmt_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_controller_property_pass(self): + """Verify dictionary return from controller property.""" + initial = { + "state": "enable", + "controller": "A", + "channel": "1", + "address": "192.168.1.1", + "subnet_mask": "255.255.255.1", + "config_method": "static"} + controller_request = [ + {"physicalLocation": {"slot": 2}, + "controllerRef": "070000000000000000000002", + "networkSettings": {"remoteAccessEnabled": True}}, + {"physicalLocation": {"slot": 1}, + "controllerRef": "070000000000000000000001", + "networkSettings": {"remoteAccessEnabled": False}}] + expected = { + 'A': {'controllerRef': '070000000000000000000001', + 'controllerSlot': 1, 'ssh': False}, + 'B': {'controllerRef': '070000000000000000000002', + 'controllerSlot': 2, 'ssh': True}} + + self._set_args(initial) + mgmt_interface = MgmtInterface() + + with mock.patch(self.REQ_FUNC, return_value=(200, controller_request)): + response = mgmt_interface.controllers + self.assertTrue(response == expected) + + def test_controller_property_fail(self): + """Verify controllers endpoint request failure causes AnsibleFailJson exception.""" + initial = { + "state": "enable", + "controller": "A", + "channel": "1", + "address": "192.168.1.1", + "subnet_mask": "255.255.255.1", + "config_method": "static"} + controller_request = [ + {"physicalLocation": {"slot": 2}, + "controllerRef": "070000000000000000000002", + "networkSettings": {"remoteAccessEnabled": True}}, + {"physicalLocation": {"slot": 1}, + "controllerRef": "070000000000000000000001", + "networkSettings": {"remoteAccessEnabled": False}}] + expected = { + 'A': {'controllerRef': '070000000000000000000001', + 'controllerSlot': 1, 'ssh': False}, + 'B': {'controllerRef': '070000000000000000000002', + 'controllerSlot': 2, 'ssh': True}} + + self._set_args(initial) + mgmt_interface = MgmtInterface() + with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to retrieve the controller settings."): + with mock.patch(self.REQ_FUNC, return_value=Exception): + response = mgmt_interface.controllers + + def test_interface_property_match_pass(self): + """Verify return value from interface property.""" + initial = { + "state": "enable", + "controller": "A", + "channel": "1", + "address": "192.168.1.1", + "subnet_mask": "255.255.255.0", + "config_method": "static"} + controller_request = [ + {"physicalLocation": {"slot": 2}, + "controllerRef": "070000000000000000000002", + "networkSettings": {"remoteAccessEnabled": True}}, + {"physicalLocation": {"slot": 1}, + "controllerRef": "070000000000000000000001", + "networkSettings": {"remoteAccessEnabled": False}}] + expected = { + "dns_servers": [{"ipv4Address": "10.1.0.250", "addressType": "ipv4"}, + {"ipv4Address": "10.10.0.20", "addressType": "ipv4"}], + "subnet_mask": "255.255.255.0", + "link_status": "up", + "ntp_servers": None, + "ntp_config_method": "disabled", + "controllerRef": "070000000000000000000001", + "config_method": "configStatic", + "enabled": True, + "gateway": "10.1.1.1", + "alias": "creG1g-AP-a", + "controllerSlot": 1, + "dns_config_method": "stat", + "id": "2800070000000000000000000001000000000000", + "address": "10.1.1.10", + "ipv6Enabled": False, + "channel": 1} + + self._set_args(initial) + mgmt_interface = MgmtInterface() + + with mock.patch(self.REQ_FUNC, side_effect=[(200, self.TEST_DATA), (200, controller_request)]): + iface = mgmt_interface.interface + self.assertTrue(iface == expected) + + def test_interface_property_request_exception_fail(self): + """Verify ethernet-interfaces endpoint request failure results in AnsibleFailJson exception.""" + initial = { + "state": "enable", + "controller": "A", + "channel": "1", + "address": "192.168.1.1", + "subnet_mask": "255.255.255.1", + "config_method": "static"} + controller_request = [ + {"physicalLocation": {"slot": 2}, + "controllerRef": "070000000000000000000002", + "networkSettings": {"remoteAccessEnabled": True}}, + {"physicalLocation": {"slot": 1}, + "controllerRef": "070000000000000000000001", + "networkSettings": {"remoteAccessEnabled": False}}] + + self._set_args(initial) + mgmt_interface = MgmtInterface() + + with self.assertRaisesRegexp(AnsibleFailJson, r"Failed to retrieve defined management interfaces."): + with mock.patch(self.REQ_FUNC, side_effect=[Exception, (200, controller_request)]): + iface = mgmt_interface.interface + + def test_interface_property_no_match_fail(self): + """Verify return value from interface property.""" + initial = { + "state": "enable", + "controller": "A", + "name": "wrong_name", + "address": "192.168.1.1", + "subnet_mask": "255.255.255.1", + "config_method": "static"} + controller_request = [ + {"physicalLocation": {"slot": 2}, + "controllerRef": "070000000000000000000002", + "networkSettings": {"remoteAccessEnabled": True}}, + {"physicalLocation": {"slot": 1}, + "controllerRef": "070000000000000000000001", + "networkSettings": {"remoteAccessEnabled": False}}] + expected = { + "dns_servers": [{"ipv4Address": "10.1.0.20", "addressType": "ipv4"}, + {"ipv4Address": "10.1.0.50", "addressType": "ipv4"}], + "subnet_mask": "255.255.255.0", + "ntp_servers": None, + "ntp_config_method": "disabled", + "controllerRef": "070000000000000000000001", + "config_method": "configStatic", + "enabled": True, + "gateway": "10.1.1.1", + "alias": "creG1g-AP-a", + "controllerSlot": 1, + "dns_config_method": "stat", + "id": "2800070000000000000000000001000000000000", + "address": "10.1.1.111", + "ipv6Enabled": False, + "channel": 1} + + self._set_args(initial) + mgmt_interface = MgmtInterface() + with self.assertRaisesRegexp(AnsibleFailJson, r"We could not find an interface matching"): + with mock.patch(self.REQ_FUNC, side_effect=[(200, self.TEST_DATA), (200, controller_request)]): + iface = mgmt_interface.interface + + def test_get_enable_interface_settings_enabled_pass(self): + """Validate get_enable_interface_settings updates properly.""" + initial = { + "state": "enable", + "controller": "A", + "name": "wrong_name", + "address": "192.168.1.1", + "subnet_mask": "255.255.255.1", + "config_method": "static"} + iface = {"enabled": False} + expected_iface = {} + + self._set_args(initial) + mgmt_interface = MgmtInterface() + + update, expected_iface, body = mgmt_interface.get_enable_interface_settings(iface, expected_iface, False, {}) + self.assertTrue(update and expected_iface["enabled"] and body["ipv4Enabled"]) + + def test_get_enable_interface_settings_disabled_pass(self): + """Validate get_enable_interface_settings updates properly.""" + initial = { + "state": "disable", + "controller": "A", + "name": "wan0", + "address": "192.168.1.1", + "subnet_mask": "255.255.255.1", + "config_method": "static"} + iface = {"enabled": True} + expected_iface = {} + + self._set_args(initial) + mgmt_interface = MgmtInterface() + + update, expected_iface, body = mgmt_interface.get_enable_interface_settings(iface, expected_iface, False, {}) + self.assertTrue(update and not expected_iface["enabled"] and not body["ipv4Enabled"]) + + def test_update_array_interface_ssh_pass(self): + """Verify get_interface_settings gives the right static configuration response.""" + initial = { + "state": "enable", + "controller": "A", + "name": "wan0", + "address": "192.168.1.1", + "subnet_mask": "255.255.255.1", + "config_method": "static", + "ssh": True} + iface = {"dns_servers": [{"ipv4Address": "10.1.0.20", "addressType": "ipv4"}, + {"ipv4Address": "10.1.0.50", "addressType": "ipv4"}], + "subnet_mask": "255.255.255.0", + "link_status": "up", + "ntp_servers": None, + "ntp_config_method": "disabled", + "controllerRef": "070000000000000000000001", + "config_method": "configStatic", + "enabled": True, + "gateway": "10.1.1.1", + "alias": "creG1g-AP-a", + "controllerSlot": 1, + "dns_config_method": "stat", + "id": "2800070000000000000000000001000000000000", + "address": "10.1.1.111", + "ipv6Enabled": False, + "channel": 1} + settings = {"controllerRef": "070000000000000000000001", + "ssh": False} + + self._set_args(initial) + mgmt_interface = MgmtInterface() + + with mock.patch(self.REQ_FUNC, return_value=(200, None)): + update = mgmt_interface.update_array(settings, iface) + self.assertTrue(update) + + def test_update_array_dns_static_ntp_disable_pass(self): + """Verify get_interface_settings gives the right static configuration response.""" + initial = { + "controller": "A", + "name": "wan0", + "dns_config_method": "static", + "dns_address": "192.168.1.1", + "dns_address_backup": "192.168.1.100", + "ntp_config_method": "disable"} + iface = {"dns_servers": [{"ipv4Address": "10.1.0.20", "addressType": "ipv4"}, + {"ipv4Address": "10.1.0.50", "addressType": "ipv4"}], + "subnet_mask": "255.255.255.0", + "link_status": "up", + "ntp_servers": None, + "ntp_config_method": "disabled", + "controllerRef": "070000000000000000000001", + "config_method": "configStatic", + "enabled": True, + "gateway": "10.1.1.1", + "alias": "creG1g-AP-a", + "controllerSlot": 1, + "dns_config_method": "configDhcp", + "id": "2800070000000000000000000001000000000000", + "address": "10.1.1.111", + "ipv6Enabled": False, + "channel": 1} + settings = {"controllerRef": "070000000000000000000001", + "ssh": False} + + self._set_args(initial) + mgmt_interface = MgmtInterface() + + with mock.patch(self.REQ_FUNC, return_value=(200, None)): + update = mgmt_interface.update_array(settings, iface) + self.assertTrue(update) + + def test_update_array_dns_dhcp_ntp_static_pass(self): + """Verify get_interface_settings gives the right static configuration response.""" + initial = { + "controller": "A", + "name": "wan0", + "ntp_config_method": "static", + "ntp_address": "192.168.1.1", + "ntp_address_backup": "192.168.1.100", + "dns_config_method": "dhcp"} + iface = {"dns_servers": [{"ipv4Address": "10.1.0.20", "addressType": "ipv4"}, + {"ipv4Address": "10.1.0.50", "addressType": "ipv4"}], + "subnet_mask": "255.255.255.0", + "link_status": "up", + "ntp_servers": None, + "ntp_config_method": "disabled", + "controllerRef": "070000000000000000000001", + "config_method": "configStatic", + "enabled": True, + "gateway": "10.1.1.1", + "alias": "creG1g-AP-a", + "controllerSlot": 1, + "dns_config_method": "configStatic", + "id": "2800070000000000000000000001000000000000", + "address": "10.1.1.111", + "ipv6Enabled": False, + "channel": 1} + settings = {"controllerRef": "070000000000000000000001", + "ssh": False} + + self._set_args(initial) + mgmt_interface = MgmtInterface() + + with mock.patch(self.REQ_FUNC, return_value=(200, None)): + update = mgmt_interface.update_array(settings, iface) + self.assertTrue(update) + + def test_update_array_dns_dhcp_ntp_static_no_change_pass(self): + """Verify get_interface_settings gives the right static configuration response.""" + initial = { + "controller": "A", + "name": "wan0", + "ntp_config_method": "dhcp", + "dns_config_method": "dhcp"} + iface = {"dns_servers": [{"ipv4Address": "10.1.0.20", "addressType": "ipv4"}, + {"ipv4Address": "10.1.0.50", "addressType": "ipv4"}], + "subnet_mask": "255.255.255.0", + "ntp_servers": None, + "ntp_config_method": "dhcp", + "controllerRef": "070000000000000000000001", + "config_method": "static", + "enabled": True, + "gateway": "10.1.1.1", + "alias": "creG1g-AP-a", + "controllerSlot": 1, + "dns_config_method": "dhcp", + "id": "2800070000000000000000000001000000000000", + "address": "10.1.1.11", + "ipv6Enabled": False, + "channel": 1} + settings = {"controllerRef": "070000000000000000000001", + "ssh": False} + + self._set_args(initial) + mgmt_interface = MgmtInterface() + + with mock.patch(self.REQ_FUNC, return_value=(200, None)): + update = mgmt_interface.update_array(settings, iface) + self.assertFalse(update) + + def test_update_array_ipv4_ipv6_disabled_fail(self): + """Verify exception is thrown when both ipv4 and ipv6 would be disabled at the same time.""" + initial = { + "state": "disable", + "controller": "A", + "name": "wan0", + "address": "192.168.1.1", + "subnet_mask": "255.255.255.1", + "config_method": "static", + "ssh": True} + iface = {"dns_servers": [{"ipv4Address": "10.1.0.20", "addressType": "ipv4"}, + {"ipv4Address": "10.1.0.50", "addressType": "ipv4"}], + "subnet_mask": "255.255.255.0", + "ntp_servers": None, + "ntp_config_method": "disabled", + "controllerRef": "070000000000000000000001", + "config_method": "configStatic", + "enabled": True, + "gateway": "10.1.1.1", + "alias": "creG1g-AP-a", + "controllerSlot": 1, + "dns_config_method": "stat", + "id": "2800070000000000000000000001000000000000", + "address": "10.1.1.11", + "ipv6Enabled": False, + "channel": 1} + settings = {"controllerRef": "070000000000000000000001", + "ssh": False} + + self._set_args(initial) + mgmt_interface = MgmtInterface() + + with self.assertRaisesRegexp(AnsibleFailJson, r"This storage-system already has IPv6 connectivity disabled."): + with mock.patch(self.REQ_FUNC, return_value=(422, dict(ipv4Enabled=False, retcode="4", errorMessage=""))): + mgmt_interface.update_array(settings, iface) + + def test_update_array_request_error_fail(self): + """Verify exception is thrown when request results in an error.""" + initial = { + "state": "disable", + "controller": "A", + "name": "wan0", + "address": "192.168.1.1", + "subnet_mask": "255.255.255.1", + "config_method": "static", + "ssh": True} + iface = {"dns_servers": [{"ipv4Address": "10.1.0.20", "addressType": "ipv4"}, + {"ipv4Address": "10.1.0.50", "addressType": "ipv4"}], + "subnet_mask": "255.255.255.0", + "ntp_servers": None, + "ntp_config_method": "disabled", + "controllerRef": "070000000000000000000001", + "config_method": "configStatic", + "enabled": True, + "gateway": "10.1.1.1", + "alias": "creG1g-AP-a", + "controllerSlot": 1, + "dns_config_method": "stat", + "id": "2800070000000000000000000001000000000000", + "address": "10.1.1.111", + "ipv6Enabled": False, + "channel": 1} + settings = {"controllerRef": "070000000000000000000001", + "ssh": False} + + self._set_args(initial) + mgmt_interface = MgmtInterface() + + with self.assertRaisesRegexp(AnsibleFailJson, r"We failed to configure the management interface."): + with mock.patch(self.REQ_FUNC, return_value=(300, dict(ipv4Enabled=False, retcode="4", errorMessage=""))): + mgmt_interface.update_array(settings, iface) + + def test_update_pass(self): + """Validate update method completes.""" + initial = { + "state": "enable", + "controller": "A", + "channel": "1", + "address": "192.168.1.1", + "subnet_mask": "255.255.255.1", + "config_method": "static", + "ssh": "yes"} + controller_request = [ + {"physicalLocation": {"slot": 2}, + "controllerRef": "070000000000000000000002", + "networkSettings": {"remoteAccessEnabled": True}}, + {"physicalLocation": {"slot": 1}, + "controllerRef": "070000000000000000000001", + "networkSettings": {"remoteAccessEnabled": False}}] + expected = { + "dns_servers": [{"ipv4Address": "10.1.0.20", "addressType": "ipv4"}, + {"ipv4Address": "10.1.0.50", "addressType": "ipv4"}], + "subnet_mask": "255.255.255.0", + "ntp_servers": None, + "ntp_config_method": "disabled", + "controllerRef": "070000000000000000000001", + "config_method": "configStatic", + "enabled": True, + "gateway": "10.1.1.1", + "alias": "creG1g-AP-a", + "controllerSlot": 1, + "dns_config_method": "stat", + "id": "2800070000000000000000000001000000000000", + "address": "10.1.1.111", + "ipv6Enabled": False, + "channel": 1} + + self._set_args(initial) + mgmt_interface = MgmtInterface() + + with self.assertRaisesRegexp(AnsibleExitJson, r"The interface settings have been updated."): + with mock.patch(self.REQ_FUNC, side_effect=[(200, None), (200, controller_request), (200, self.TEST_DATA), + (200, controller_request), (200, self.TEST_DATA)]): + mgmt_interface.update()