From 1f2ae0d4cd50810cd823a4705645dc9699fa06a2 Mon Sep 17 00:00:00 2001 From: Michael Price Date: Tue, 28 Aug 2018 09:38:53 -0500 Subject: [PATCH] Define a module for managing E-Series settings (#41010) There are multiple settings that are defined at a global level for E-Series systems, but don't necessarily fit with anything else. This module is intended to provide a place to encapsulate those. --- .../modules/storage/netapp/netapp_e_global.py | 157 ++++++++++++++++++ .../targets/netapp_eseries_global/aliases | 10 ++ .../netapp_eseries_global/tasks/main.yml | 1 + .../netapp_eseries_global/tasks/run.yml | 51 ++++++ .../storage/netapp/test_netapp_e_global.py | 76 +++++++++ 5 files changed, 295 insertions(+) create mode 100644 lib/ansible/modules/storage/netapp/netapp_e_global.py create mode 100644 test/integration/targets/netapp_eseries_global/aliases create mode 100644 test/integration/targets/netapp_eseries_global/tasks/main.yml create mode 100644 test/integration/targets/netapp_eseries_global/tasks/run.yml create mode 100644 test/units/modules/storage/netapp/test_netapp_e_global.py diff --git a/lib/ansible/modules/storage/netapp/netapp_e_global.py b/lib/ansible/modules/storage/netapp/netapp_e_global.py new file mode 100644 index 00000000000..a70820c2a01 --- /dev/null +++ b/lib/ansible/modules/storage/netapp/netapp_e_global.py @@ -0,0 +1,157 @@ +#!/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_global +short_description: NetApp E-Series manage global settings configuration +description: + - Allow the user to configure several of the global settings associated with an E-Series storage-system +version_added: '2.7' +author: Michael Price (@lmprice) +extends_documentation_fragment: + - netapp.eseries +options: + name: + description: + - Set the name of the E-Series storage-system + - This label/name doesn't have to be unique. + - May be up to 30 characters in length. + aliases: + - label + log_path: + description: + - A local path to a file to be used for debug logging + required: no +notes: + - Check mode is supported. + - This module requires Web Services API v1.3 or newer. +""" + +EXAMPLES = """ + - name: Set the storage-system name + netapp_e_global: + name: myArrayName + api_url: "10.1.1.1:8443" + api_username: "admin" + api_password: "myPass" +""" + +RETURN = """ +msg: + description: Success message + returned: on success + type: string + sample: The settings have been updated. +name: + description: + - The current name/label of the storage-system. + returned: on success + sample: myArrayName + type: str +""" +import json +import logging + +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 GlobalSettings(object): + def __init__(self): + argument_spec = eseries_host_argument_spec() + argument_spec.update(dict( + name=dict(type='str', required=False, aliases=['label']), + log_path=dict(type='str', required=False), + )) + + self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, ) + args = self.module.params + self.name = args['name'] + + 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 + + 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.name and len(self.name) > 30: + self.module.fail_json(msg="The provided name is invalid, it must be < 30 characters in length.") + + def get_name(self): + try: + (rc, result) = request(self.url + 'storage-systems/%s' % self.ssid, headers=HEADERS, **self.creds) + if result['status'] in ['offline', 'neverContacted']: + self.module.fail_json(msg="This storage-system is offline! Array Id [%s]." % (self.ssid)) + return result['name'] + except Exception as err: + self.module.fail_json(msg="Connection failure! Array Id [%s]. Error [%s]." % (self.ssid, to_native(err))) + + def update_name(self): + name = self.get_name() + update = False + if self.name != name: + update = True + + body = dict(name=self.name) + + if update and not self.check_mode: + try: + (rc, result) = request(self.url + 'storage-systems/%s/configuration' % self.ssid, method='POST', + data=json.dumps(body), headers=HEADERS, **self.creds) + self._logger.info("Set name to %s.", result['name']) + # This is going to catch cases like a connection failure + except Exception as err: + self.module.fail_json( + msg="We failed to set the storage-system name! Array Id [%s]. Error [%s]." + % (self.ssid, to_native(err))) + return update + + def update(self): + update = self.update_name() + name = self.get_name() + + self.module.exit_json(msg="The requested settings have been updated.", changed=update, name=name) + + def __call__(self, *args, **kwargs): + self.update() + + +def main(): + settings = GlobalSettings() + settings() + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/netapp_eseries_global/aliases b/test/integration/targets/netapp_eseries_global/aliases new file mode 100644 index 00000000000..d314d14a748 --- /dev/null +++ b/test/integration/targets/netapp_eseries_global/aliases @@ -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 diff --git a/test/integration/targets/netapp_eseries_global/tasks/main.yml b/test/integration/targets/netapp_eseries_global/tasks/main.yml new file mode 100644 index 00000000000..996354c8860 --- /dev/null +++ b/test/integration/targets/netapp_eseries_global/tasks/main.yml @@ -0,0 +1 @@ +- include_tasks: run.yml diff --git a/test/integration/targets/netapp_eseries_global/tasks/run.yml b/test/integration/targets/netapp_eseries_global/tasks/run.yml new file mode 100644 index 00000000000..6a57b2cf855 --- /dev/null +++ b/test/integration/targets/netapp_eseries_global/tasks/run.yml @@ -0,0 +1,51 @@ +# 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) + +- name: NetApp Test Global Settings 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: &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 + name: TestArray +- name: set credentials + set_fact: + credentials: *creds + +- name: Show some debug information + debug: + msg: "Using user={{ credentials.api_username }} on server={{ credentials.api_url }}." + +- name: Set the name to the default + netapp_e_global: + <<: *creds + +- name: Set a few different names + netapp_e_global: + <<: *creds + name: "{{ item }}" + loop: + - a + - x + - "000001111122222333334444455555" + +- name: Set an explicit name + netapp_e_global: + <<: *creds + name: abc + register: result + +- name: Validate name + assert: + that: result.name == "abc" + +- name: Restore the original name + netapp_e_global: + <<: *creds \ No newline at end of file diff --git a/test/units/modules/storage/netapp/test_netapp_e_global.py b/test/units/modules/storage/netapp/test_netapp_e_global.py new file mode 100644 index 00000000000..d3027f22ca5 --- /dev/null +++ b/test/units/modules/storage/netapp/test_netapp_e_global.py @@ -0,0 +1,76 @@ +# (c) 2018, NetApp Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +from ansible.modules.storage.netapp.netapp_e_global import GlobalSettings +from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args + +__metaclass__ = type +from ansible.compat.tests import mock + + +class GlobalSettingsTest(ModuleTestCase): + REQUIRED_PARAMS = { + 'api_username': 'rw', + 'api_password': 'password', + 'api_url': 'http://localhost', + 'ssid': '1', + } + REQ_FUNC = 'ansible.modules.storage.netapp.netapp_e_global.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_set_name(self): + """Ensure we can successfully set the name""" + self._set_args(dict(name="x")) + + expected = dict(name='y', status='online') + namer = GlobalSettings() + # Expecting an update + with mock.patch(self.REQ_FUNC, return_value=(200, expected)) as req: + with mock.patch.object(namer, 'get_name', return_value='y'): + update = namer.update_name() + self.assertTrue(update) + # Expecting no update + with mock.patch(self.REQ_FUNC, return_value=(200, expected)) as req: + with mock.patch.object(namer, 'get_name', return_value='x'): + update = namer.update_name() + self.assertFalse(update) + + # Expecting an update, but no actual calls, since we're using check_mode=True + namer.check_mode = True + with mock.patch(self.REQ_FUNC, return_value=(200, expected)) as req: + with mock.patch.object(namer, 'get_name', return_value='y'): + update = namer.update_name() + self.assertEquals(0, req.called) + self.assertTrue(update) + + def test_get_name(self): + """Ensure we can successfully set the name""" + self._set_args() + + expected = dict(name='y', status='online') + namer = GlobalSettings() + + with mock.patch(self.REQ_FUNC, return_value=(200, expected)) as req: + name = namer.get_name() + self.assertEquals(name, expected['name']) + + def test_get_name_fail(self): + """Ensure we can successfully set the name""" + self._set_args() + + expected = dict(name='y', status='offline') + namer = GlobalSettings() + + with self.assertRaises(AnsibleFailJson): + with mock.patch(self.REQ_FUNC, side_effect=Exception()) as req: + name = namer.get_name() + + with self.assertRaises(AnsibleFailJson): + with mock.patch(self.REQ_FUNC, return_value=(200, expected)) as req: + update = namer.update_name()