diff --git a/lib/ansible/modules/storage/netapp/netapp_e_asup.py b/lib/ansible/modules/storage/netapp/netapp_e_asup.py new file mode 100644 index 0000000000..1f921a6333 --- /dev/null +++ b/lib/ansible/modules/storage/netapp/netapp_e_asup.py @@ -0,0 +1,309 @@ +#!/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_asup +short_description: manage E-Series auto-support settings +description: + - Allow the auto-support settings to be configured for an individual E-Series storage-system +version_added: '2.7' +author: Michael Price (@lmprice) +extends_documentation_fragment: + - netapp.eseries +options: + state: + description: + - Enable/disable the E-Series auto-support configuration. + - When this option is enabled, configuration, logs, and other support-related information will be relayed + to NetApp to help better support your system. No personally identifiable information, passwords, etc, will + be collected. + default: enabled + choices: + - enabled + - disabled + aliases: + - asup + - auto_support + - autosupport + active: + description: + - Enable active/proactive monitoring for ASUP. When a problem is detected by our monitoring systems, it's + possible that the bundle did not contain all of the required information at the time of the event. + Enabling this option allows NetApp support personnel to manually request transmission or re-transmission + of support data in order ot resolve the problem. + - Only applicable if I(state=enabled). + default: yes + type: bool + start: + description: + - A start hour may be specified in a range from 0 to 23 hours. + - ASUP bundles will be sent daily between the provided start and end time (UTC). + - I(start) must be less than I(end). + aliases: + - start_time + default: 0 + end: + description: + - An end hour may be specified in a range from 1 to 24 hours. + - ASUP bundles will be sent daily between the provided start and end time (UTC). + - I(start) must be less than I(end). + aliases: + - end_time + default: 24 + days: + description: + - A list of days of the week that ASUP bundles will be sent. A larger, weekly bundle will be sent on one + of the provided days. + choices: + - monday + - tuesday + - wednesday + - thursday + - friday + - saturday + - sunday + required: no + aliases: + - days_of_week + - schedule_days + verbose: + description: + - Provide the full ASUP configuration in the return. + default: no + required: no + type: bool + log_path: + description: + - A local path to a file to be used for debug logging + required: no +notes: + - Check mode is supported. + - Enabling ASUP will allow our support teams to monitor the logs of the storage-system in order to proactively + respond to issues with the system. It is recommended that all ASUP-related options be enabled, but they may be + disabled if desired. + - This API is currently only supported with the Embedded Web Services API v2.0 and higher. +""" + +EXAMPLES = """ + - name: Enable ASUP and allow pro-active retrieval of bundles + netapp_e_asup: + state: enabled + active: yes + api_url: "10.1.1.1:8443" + api_username: "admin" + api_password: "myPass" + + - name: Set the ASUP schedule to only send bundles from 12 AM CST to 3 AM CST. + netapp_e_asup: + start: 17 + end: 20 + 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. +asup: + description: + - True if ASUP is enabled. + returned: on success + sample: True + type: bool +active: + description: + - True if the active option has been enabled. + returned: on success + sample: True + type: bool +cfg: + description: + - Provide the full ASUP configuration. + returned: on success when I(verbose=true). + type: complex + contains: + asupEnabled: + description: + - True if ASUP has been enabled. + type: bool + onDemandEnabled: + description: + - True if ASUP active monitoring has been enabled. + type: bool + daysOfWeek: + description: + - The days of the week that ASUP bundles will be sent. + type: list +""" + +import json +import logging +from pprint import pformat + +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 Asup(object): + DAYS_OPTIONS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'] + + def __init__(self): + argument_spec = eseries_host_argument_spec() + argument_spec.update(dict( + state=dict(type='str', required=False, default='enabled', aliases=['asup', 'auto_support', 'autosupport'], + choices=['enabled', 'disabled']), + active=dict(type='bool', required=False, default=True, ), + days=dict(type='list', required=False, aliases=['schedule_days', 'days_of_week'], + choices=self.DAYS_OPTIONS), + start=dict(type='int', required=False, default=0, aliases=['start_time']), + end=dict(type='int', required=False, default=24, aliases=['end_time']), + verbose=dict(type='bool', required=False, default=False), + log_path=dict(type='str', required=False), + )) + + self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, ) + args = self.module.params + self.asup = args['state'] == 'enabled' + self.active = args['active'] + self.days = args['days'] + self.start = args['start'] + self.end = args['end'] + self.verbose = args['verbose'] + + 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.start >= self.end: + self.module.fail_json(msg="The value provided for the start time is invalid." + " It must be less than the end time.") + if self.start < 0 or self.start > 23: + self.module.fail_json(msg="The value provided for the start time is invalid. It must be between 0 and 23.") + else: + self.start = self.start * 60 + if self.end < 1 or self.end > 24: + self.module.fail_json(msg="The value provided for the end time is invalid. It must be between 1 and 24.") + else: + self.end = min(self.end * 60, 1439) + + if not self.days: + self.days = self.DAYS_OPTIONS + + def get_configuration(self): + try: + (rc, result) = request(self.url + 'device-asup', headers=HEADERS, **self.creds) + + if not (result['asupCapable'] and result['onDemandCapable']): + self.module.fail_json(msg="ASUP is not supported on this device. Array Id [%s]." % (self.ssid)) + return result + + except Exception as err: + self.module.fail_json(msg="Failed to retrieve ASUP configuration! Array Id [%s]. Error [%s]." + % (self.ssid, to_native(err))) + + def update_configuration(self): + config = self.get_configuration() + update = False + body = dict() + + if self.asup: + body = dict(asupEnabled=True) + if not config['asupEnabled']: + update = True + + if (config['onDemandEnabled'] and config['remoteDiagsEnabled']) != self.active: + update = True + body.update(dict(onDemandEnabled=self.active, + remoteDiagsEnabled=self.active)) + self.days.sort() + config['schedule']['daysOfWeek'].sort() + + body['schedule'] = dict(daysOfWeek=self.days, + dailyMinTime=self.start, + dailyMaxTime=self.end, + weeklyMinTime=self.start, + weeklyMaxTime=self.end) + + if self.days != config['schedule']['daysOfWeek']: + update = True + if self.start != config['schedule']['dailyMinTime'] or self.start != config['schedule']['weeklyMinTime']: + update = True + elif self.end != config['schedule']['dailyMaxTime'] or self.end != config['schedule']['weeklyMaxTime']: + update = True + + elif config['asupEnabled']: + body = dict(asupEnabled=False) + update = True + + self._logger.info(pformat(body)) + + if update and not self.check_mode: + try: + (rc, result) = request(self.url + 'device-asup', method='POST', + data=json.dumps(body), headers=HEADERS, **self.creds) + # 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_configuration() + cfg = self.get_configuration() + if self.verbose: + self.module.exit_json(msg="The ASUP settings have been updated.", changed=update, + asup=cfg['asupEnabled'], active=cfg['onDemandEnabled'], cfg=cfg) + else: + self.module.exit_json(msg="The ASUP settings have been updated.", changed=update, + asup=cfg['asupEnabled'], active=cfg['onDemandEnabled']) + + def __call__(self, *args, **kwargs): + self.update() + + +def main(): + settings = Asup() + settings() + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/netapp_eseries_asup/aliases b/test/integration/targets/netapp_eseries_asup/aliases new file mode 100644 index 0000000000..d314d14a74 --- /dev/null +++ b/test/integration/targets/netapp_eseries_asup/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_asup/tasks/main.yml b/test/integration/targets/netapp_eseries_asup/tasks/main.yml new file mode 100644 index 0000000000..996354c886 --- /dev/null +++ b/test/integration/targets/netapp_eseries_asup/tasks/main.yml @@ -0,0 +1 @@ +- include_tasks: run.yml diff --git a/test/integration/targets/netapp_eseries_asup/tasks/run.yml b/test/integration/targets/netapp_eseries_asup/tasks/run.yml new file mode 100644 index 0000000000..e3325ac8a7 --- /dev/null +++ b/test/integration/targets/netapp_eseries_asup/tasks/run.yml @@ -0,0 +1,233 @@ +# 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 ASUP 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: set credentials + set_fact: + credentials: *creds + +- name: Show some debug information + debug: + msg: "Using user={{ credentials.api_username }} on server={{ credentials.api_url }}." + +# **************************************************** +# *** Enable auto-support using all default values *** +# **************************************************** +- name: Enable auto-support using default values + netapp_e_asup: + <<: *creds + verbose: true + +- name: Collect auto-support state information from the array + uri: + url: "{{ credentials.api_url }}/device-asup" + user: "{{ credentials.api_username }}" + password: "{{ credentials.api_password }}" + body_format: json + validate_certs: no + register: current + +- name: Validate auto-support expected default state + assert: + that: "{{ current.json.asupEnabled and + current.json.onDemandEnabled and + current.json.remoteDiagsEnabled and + current.json.schedule.dailyMinTime == 0 and + current.json.schedule.dailyMaxTime == 1439 }}" + msg: "Unexpected auto-support state" + +- name: Validate auto-support schedule + assert: + that: "{{ item in current.json.schedule.daysOfWeek }}" + msg: "{{ item }} is missing from the schedule" + loop: "{{ lookup('list', ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']) }}" + +# **************************** +# *** Disable auto-support *** +# **************************** +- name: Disable auto-support + netapp_e_asup: + <<: *creds + state: disabled + +- name: Collect auto-support state information from the array + uri: + url: "{{ credentials.api_url }}/device-asup" + user: "{{ credentials.api_username }}" + password: "{{ credentials.api_password }}" + body_format: json + validate_certs: no + register: current + +- name: Validate auto-support is disabled + assert: + that: "{{ not current.json.asupEnabled }}" + msg: "Auto-support failed to be disabled" + +# **************************************************** +# *** Enable auto-support using specific values *** +# **************************************************** +- name: Enable auto-support using specific values + netapp_e_asup: + <<: *creds + state: enabled + active: true + start: 22 + end: 24 + days: + - friday + - saturday + verbose: true + +- name: Collect auto-support state information from the array + uri: + url: "{{ credentials.api_url }}/device-asup" + user: "{{ credentials.api_username }}" + password: "{{ credentials.api_password }}" + body_format: json + validate_certs: no + register: current + +- name: Validate auto-support expected state + assert: + that: "{{ current.json.asupEnabled and + current.json.onDemandEnabled and + current.json.remoteDiagsEnabled and + current.json.schedule.dailyMinTime == (22 * 60) and + current.json.schedule.dailyMaxTime == (24 * 60 - 1) }}" + msg: "Unexpected auto-support state" + +- name: Validate auto-support schedule + assert: + that: "{{ item in current.json.schedule.daysOfWeek }}" + msg: "{{ item }} is missing from the schedule" + loop: "{{ lookup('list', ['friday', 'saturday']) }}" + +# *********************************** +# *** Alter auto-support schedule *** +# *********************************** +- name: Auto auto-support schedule + netapp_e_asup: + <<: *creds + state: enabled + active: true + start: 0 + end: 5 + days: + - monday + - thursday + - sunday + verbose: true + +- name: Collect auto-support state information from the array + uri: + url: "{{ credentials.api_url }}/device-asup" + user: "{{ credentials.api_username }}" + password: "{{ credentials.api_password }}" + body_format: json + validate_certs: no + register: current + +- name: Validate auto-support expected state + assert: + that: "{{ current.json.asupEnabled and + current.json.onDemandEnabled and + current.json.remoteDiagsEnabled and + current.json.schedule.dailyMinTime == (0 * 60) and + current.json.schedule.dailyMaxTime == (5 * 60) }}" + msg: "Unexpected auto-support state" + +- name: Validate auto-support schedule + assert: + that: "{{ item in current.json.schedule.daysOfWeek }}" + msg: "{{ item }} is missing from the schedule" + loop: "{{ lookup('list', ['monday', 'thursday', 'sunday']) }}" + +# ************************************************************* +# *** Repeat previous test to verify state remains the same *** +# ************************************************************* +- name: Repeat auto-support schedule change to verify idempotency + netapp_e_asup: + <<: *creds + state: enabled + active: true + start: 0 + end: 5 + days: + - monday + - thursday + - sunday + verbose: true + register: result + +- name: Collect auto-support state information from the array + uri: + url: "{{ credentials.api_url }}/device-asup" + user: "{{ credentials.api_username }}" + password: "{{ credentials.api_password }}" + body_format: json + validate_certs: no + register: current + +- name: Validate auto-support expected state + assert: + that: "{{ current.json.asupEnabled and + current.json.onDemandEnabled and + current.json.remoteDiagsEnabled and + current.json.schedule.dailyMinTime == (0 * 60) and + current.json.schedule.dailyMaxTime == (5 * 60) }}" + msg: "Unexpected auto-support state" + +- name: Validate auto-support schedule + assert: + that: "{{ item in current.json.schedule.daysOfWeek }}" + msg: "{{ item }} is missing from the schedule" + loop: "{{ lookup('list', ['monday', 'thursday', 'sunday']) }}" + +- name: Validate change was not detected + assert: + that: "{{ not result.changed }}" + msg: "Invalid change was detected" + +# *********************************** +# *** Disable auto-support active *** +# *********************************** +- name: Auto auto-support schedule + netapp_e_asup: + <<: *creds + state: enabled + active: false + start: 0 + end: 5 + days: + - monday + - thursday + - sunday + verbose: true + +- name: Collect auto-support state information from the array + uri: + url: "{{ credentials.api_url }}/device-asup" + user: "{{ credentials.api_username }}" + password: "{{ credentials.api_password }}" + body_format: json + validate_certs: no + register: current + +- name: Validate auto-support expected state + assert: + that: "{{ current.json.asupEnabled and not current.json.onDemandEnabled and not current.json.remoteDiagsEnabled }}" + msg: "Unexpected auto-support state" diff --git a/test/units/modules/storage/netapp/test_netapp_e_asup.py b/test/units/modules/storage/netapp/test_netapp_e_asup.py new file mode 100644 index 0000000000..bcc170f5bd --- /dev/null +++ b/test/units/modules/storage/netapp/test_netapp_e_asup.py @@ -0,0 +1,181 @@ +# (c) 2018, NetApp Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +import json + +from ansible.modules.storage.netapp.netapp_e_asup import Asup +from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args + +__metaclass__ = type +from ansible.compat.tests import mock + + +class AsupTest(ModuleTestCase): + REQUIRED_PARAMS = { + 'api_username': 'rw', + 'api_password': 'password', + 'api_url': 'http://localhost', + 'ssid': '1', + } + REQ_FUNC = 'ansible.modules.storage.netapp.netapp_e_asup.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_get_config_asup_capable_false(self): + """Ensure we fail correctly if ASUP is not available on this platform""" + self._set_args() + + expected = dict(asupCapable=False, onDemandCapable=True) + asup = Asup() + # Expecting an update + with self.assertRaisesRegexp(AnsibleFailJson, r"not supported"): + with mock.patch(self.REQ_FUNC, return_value=(200, expected)): + asup.get_configuration() + + def test_get_config_on_demand_capable_false(self): + """Ensure we fail correctly if ASUP is not available on this platform""" + self._set_args() + + expected = dict(asupCapable=True, onDemandCapable=False) + asup = Asup() + # Expecting an update + with self.assertRaisesRegexp(AnsibleFailJson, r"not supported"): + with mock.patch(self.REQ_FUNC, return_value=(200, expected)): + asup.get_configuration() + + def test_get_config(self): + """Validate retrieving the ASUP configuration""" + self._set_args() + + expected = dict(asupCapable=True, onDemandCapable=True) + asup = Asup() + + with mock.patch(self.REQ_FUNC, return_value=(200, expected)): + config = asup.get_configuration() + self.assertEquals(config, expected) + + def test_update_configuration(self): + """Validate retrieving the ASUP configuration""" + self._set_args(dict(asup='enabled')) + + expected = dict() + initial = dict(asupCapable=True, + asupEnabled=True, + onDemandEnabled=False, + remoteDiagsEnabled=False, + schedule=dict(daysOfWeek=[], dailyMinTime=0, weeklyMinTime=0, dailyMaxTime=24, weeklyMaxTime=24)) + asup = Asup() + + with mock.patch(self.REQ_FUNC, return_value=(200, expected)) as req: + with mock.patch.object(asup, 'get_configuration', return_value=initial): + updated = asup.update_configuration() + self.assertTrue(req.called) + self.assertTrue(updated) + + def test_update_configuration_asup_disable(self): + """Validate retrieving the ASUP configuration""" + self._set_args(dict(asup='disabled')) + + expected = dict() + initial = dict(asupCapable=True, + asupEnabled=True, + onDemandEnabled=False, + remoteDiagsEnabled=False, + schedule=dict(daysOfWeek=[], dailyMinTime=0, weeklyMinTime=0, dailyMaxTime=24, weeklyMaxTime=24)) + asup = Asup() + + with mock.patch(self.REQ_FUNC, return_value=(200, expected)) as req: + with mock.patch.object(asup, 'get_configuration', return_value=initial): + updated = asup.update_configuration() + self.assertTrue(updated) + + self.assertTrue(req.called) + + # Ensure it was called with the right arguments + called_with = req.call_args + body = json.loads(called_with[1]['data']) + self.assertFalse(body['asupEnabled']) + + def test_update_configuration_enable(self): + """Validate retrieving the ASUP configuration""" + self._set_args(dict(asup='enabled')) + + expected = dict() + initial = dict(asupCapable=False, + asupEnabled=False, + onDemandEnabled=False, + remoteDiagsEnabled=False, + schedule=dict(daysOfWeek=[], dailyMinTime=0, weeklyMinTime=0, dailyMaxTime=24, weeklyMaxTime=24)) + asup = Asup() + + with mock.patch(self.REQ_FUNC, return_value=(200, expected)) as req: + with mock.patch.object(asup, 'get_configuration', return_value=initial): + updated = asup.update_configuration() + self.assertTrue(updated) + + self.assertTrue(req.called) + + # Ensure it was called with the right arguments + called_with = req.call_args + body = json.loads(called_with[1]['data']) + self.assertTrue(body['asupEnabled']) + self.assertTrue(body['onDemandEnabled']) + self.assertTrue(body['remoteDiagsEnabled']) + + def test_update_configuration_request_exception(self): + """Validate exception handling when request throws an exception.""" + config_response = dict(asupEnabled=True, + onDemandEnabled=True, + remoteDiagsEnabled=True, + schedule=dict(daysOfWeek=[], + dailyMinTime=0, + weeklyMinTime=0, + dailyMaxTime=24, + weeklyMaxTime=24)) + + self._set_args(dict(state="enabled")) + asup = Asup() + with self.assertRaises(Exception): + with mock.patch.object(asup, 'get_configuration', return_value=config_response): + with mock.patch(self.REQ_FUNC, side_effect=Exception): + asup.update_configuration() + + def test_init_schedule(self): + """Validate schedule correct schedule initialization""" + self._set_args(dict(state="enabled", active=True, days=["sunday", "monday", "tuesday"], start=20, end=24)) + asup = Asup() + + self.assertTrue(asup.asup) + self.assertEquals(asup.days, ["sunday", "monday", "tuesday"]), + self.assertEquals(asup.start, 1200) + self.assertEquals(asup.end, 1439) + + def test_init_schedule_invalid(self): + """Validate updating ASUP with invalid schedule fails test.""" + self._set_args(dict(state="enabled", active=True, start=22, end=20)) + with self.assertRaisesRegexp(AnsibleFailJson, r"start time is invalid"): + Asup() + + def test_init_schedule_days_invalid(self): + """Validate updating ASUP with invalid schedule fails test.""" + self._set_args(dict(state="enabled", active=True, days=["someday", "thataday", "nonday"])) + with self.assertRaises(AnsibleFailJson): + Asup() + + def test_update(self): + """Validate updating ASUP with valid schedule passes""" + initial = dict(asupCapable=True, + onDemandCapable=True, + asupEnabled=True, + onDemandEnabled=False, + remoteDiagsEnabled=False, + schedule=dict(daysOfWeek=[], dailyMinTime=0, weeklyMinTime=0, dailyMaxTime=24, weeklyMaxTime=24)) + self._set_args(dict(state="enabled", active=True, days=["sunday", "monday", "tuesday"], start=10, end=20)) + asup = Asup() + with self.assertRaisesRegexp(AnsibleExitJson, r"ASUP settings have been updated"): + with mock.patch(self.REQ_FUNC, return_value=(200, dict(asupCapable=True))): + with mock.patch.object(asup, "get_configuration", return_value=initial): + asup.update()