From 7eda94dc8d6f87fdf0508e44a897e39438c04f13 Mon Sep 17 00:00:00 2001 From: ndswartz Date: Tue, 28 Aug 2018 07:22:36 -0500 Subject: [PATCH] Defined netapp_e_syslog storage module (#42421) Module allows syslog server configuration on NetApp E-Series storage arrays. --- .../modules/storage/netapp/netapp_e_syslog.py | 280 ++++++++++++++++++ .../storage/netapp/test_netapp_e_syslog.py | 126 ++++++++ 2 files changed, 406 insertions(+) create mode 100644 lib/ansible/modules/storage/netapp/netapp_e_syslog.py create mode 100644 test/units/modules/storage/netapp/test_netapp_e_syslog.py diff --git a/lib/ansible/modules/storage/netapp/netapp_e_syslog.py b/lib/ansible/modules/storage/netapp/netapp_e_syslog.py new file mode 100644 index 0000000000..8e9d444ee9 --- /dev/null +++ b/lib/ansible/modules/storage/netapp/netapp_e_syslog.py @@ -0,0 +1,280 @@ +#!/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_syslog +short_description: NetApp E-Series manage syslog settings +description: + - Allow the syslog settings to be configured for an individual E-Series storage-system +version_added: '2.7' +author: Nathan Swartz (@ndswartz) +extends_documentation_fragment: + - netapp.eseries +options: + state: + description: + - Add or remove the syslog server configuration for E-Series storage array. + - Existing syslog server configuration will be removed or updated when its address matches I(address). + - Fully qualified hostname that resolve to an IPv4 address that matches I(address) will not be + treated as a match. + choices: + - present + - absent + default: present + address: + description: + - The syslog server's IPv4 address or a fully qualified hostname. + - All existing syslog configurations will be removed when I(state=absent) and I(address=None). + port: + description: + - This is the port the syslog server is using. + default: 514 + protocol: + description: + - This is the transmission protocol the syslog server's using to receive syslog messages. + choices: + - udp + - tcp + - tls + default: udp + components: + description: + - The e-series logging components define the specific logs to transfer to the syslog server. + - At the time of writing, 'auditLog' is the only logging component but more may become available. + default: ["auditLog"] + test: + description: + - This forces a test syslog message to be sent to the stated syslog server. + - Only attempts transmission when I(state=present). + type: bool + default: no + log_path: + description: + - This argument specifies a local path for logging purposes. + required: no +notes: + - Check mode is supported. + - This API is currently only supported with the Embedded Web Services API v2.12 (bundled with + SANtricity OS 11.40.2) and higher. +""" + +EXAMPLES = """ + - name: Add two syslog server configurations to NetApp E-Series storage array. + netapp_e_syslog: + state: present + address: "{{ item }}" + port: 514 + protocol: tcp + component: "auditLog" + api_url: "10.1.1.1:8443" + api_username: "admin" + api_password: "myPass" + loop: + - "192.168.1.1" + - "192.168.1.100" +""" + +RETURN = """ +msg: + description: Success message + returned: on success + type: string + sample: The settings have been updated. +syslog: + description: + - True if syslog server configuration has been added to e-series storage array. + returned: on success + sample: True + type: bool +""" + +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 Syslog(object): + def __init__(self): + argument_spec = eseries_host_argument_spec() + argument_spec.update(dict( + state=dict(choices=["present", "absent"], required=False, default="present"), + address=dict(type="str", required=False), + port=dict(type="int", default=514, required=False), + protocol=dict(choices=["tcp", "tls", "udp"], default="udp", required=False), + components=dict(type="list", required=False, default=["auditLog"]), + test=dict(type="bool", default=False, require=False), + log_path=dict(type="str", required=False), + )) + + required_if = [ + ["state", "present", ["address", "port", "protocol", "components"]], + ] + + mutually_exclusive = [ + ["test", "absent"], + ] + + self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if, + mutually_exclusive=mutually_exclusive) + args = self.module.params + + self.syslog = args["state"] in ["present"] + self.address = args["address"] + self.port = args["port"] + self.protocol = args["protocol"] + self.components = args["components"] + self.test = args["test"] + 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.components.sort() + + self.check_mode = self.module.check_mode + + # logging setup + log_path = args["log_path"] + 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 += '/' + + def get_configuration(self): + """Retrieve existing syslog configuration.""" + try: + (rc, result) = request(self.url + "storage-systems/{0}/syslog".format(self.ssid), + headers=HEADERS, **self.creds) + return result + except Exception as err: + self.module.fail_json(msg="Failed to retrieve syslog configuration! Array Id [%s]. Error [%s]." + % (self.ssid, to_native(err))) + + def test_configuration(self, body): + """Send test syslog message to the storage array. + + Allows fix number of retries to occur before failure is issued to give the storage array time to create + new syslog server record. + """ + try: + (rc, result) = request(self.url + "storage-systems/{0}/syslog/{1}/test".format(self.ssid, body["id"]), + method='POST', headers=HEADERS, **self.creds) + except Exception as err: + self.module.fail_json( + msg="We failed to send test message! Array Id [{0}]. Error [{1}].".format(self.ssid, to_native(err))) + + def update_configuration(self): + """Post the syslog request to array.""" + config_match = None + perfect_match = None + update = False + body = dict() + + # search existing configuration for syslog server entry match + configs = self.get_configuration() + if self.address: + for config in configs: + if config["serverAddress"] == self.address: + config_match = config + if (config["port"] == self.port and config["protocol"] == self.protocol and + len(config["components"]) == len(self.components) and + all([component["type"] in self.components for component in config["components"]])): + perfect_match = config_match + break + + # generate body for the http request + if self.syslog: + if not perfect_match: + update = True + if config_match: + body.update(dict(id=config_match["id"])) + components = [dict(type=component_type) for component_type in self.components] + body.update(dict(serverAddress=self.address, port=self.port, + protocol=self.protocol, components=components)) + self._logger.info(body) + self.make_configuration_request(body) + + # remove specific syslog server configuration + elif self.address: + update = True + body.update(dict(id=config_match["id"])) + self._logger.info(body) + self.make_configuration_request(body) + + # if no address is specified, remove all syslog server configurations + elif configs: + update = True + for config in configs: + body.update(dict(id=config["id"])) + self._logger.info(body) + self.make_configuration_request(body) + + return update + + def make_configuration_request(self, body): + # make http request(s) + if not self.check_mode: + try: + if self.syslog: + if "id" in body: + (rc, result) = request( + self.url + "storage-systems/{0}/syslog/{1}".format(self.ssid, body["id"]), + method='POST', data=json.dumps(body), headers=HEADERS, **self.creds) + else: + (rc, result) = request(self.url + "storage-systems/{0}/syslog".format(self.ssid), + method='POST', data=json.dumps(body), headers=HEADERS, **self.creds) + body.update(result) + + # send syslog test message + if self.test: + self.test_configuration(body) + + elif "id" in body: + (rc, result) = request(self.url + "storage-systems/{0}/syslog/{1}".format(self.ssid, body["id"]), + method='DELETE', 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 modify syslog configuration! Array Id [%s]. Error [%s]." + % (self.ssid, to_native(err))) + + def update(self): + """Update configuration and respond to ansible.""" + update = self.update_configuration() + self.module.exit_json(msg="The syslog settings have been updated.", changed=update) + + def __call__(self, *args, **kwargs): + self.update() + + +def main(): + settings = Syslog() + settings() + + +if __name__ == "__main__": + main() diff --git a/test/units/modules/storage/netapp/test_netapp_e_syslog.py b/test/units/modules/storage/netapp/test_netapp_e_syslog.py new file mode 100644 index 0000000000..1a8c8762dd --- /dev/null +++ b/test/units/modules/storage/netapp/test_netapp_e_syslog.py @@ -0,0 +1,126 @@ +# (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_syslog import Syslog +from ansible.module_utils.six.moves.urllib.error import HTTPError +from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args +import time + +__metaclass__ = type +from ansible.compat.tests import mock + + +class AsupTest(ModuleTestCase): + REQUIRED_PARAMS = { + "api_username": "rw", + "api_password": "password", + "api_url": "http://localhost", + } + REQ_FUNC = 'ansible.modules.storage.netapp.netapp_e_syslog.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_test_configuration_fail(self): + """Validate test_configuration fails when request exception is thrown.""" + initial = {"state": "present", + "ssid": "1", + "address": "192.168.1.1", + "port": "514", + "protocol": "udp", + "components": ["auditLog"]} + self._set_args(initial) + syslog = Syslog() + + with self.assertRaisesRegexp(AnsibleFailJson, r"We failed to send test message!"): + with mock.patch(self.REQ_FUNC, side_effect=Exception()): + with mock.patch("time.sleep", return_value=None): # mocking sleep is not working + syslog.test_configuration(self.REQUIRED_PARAMS) + + def test_update_configuration_record_match_pass(self): + """Verify existing syslog server record match does not issue update request.""" + initial = {"state": "present", + "ssid": "1", + "address": "192.168.1.1", + "port": "514", + "protocol": "udp", + "components": ["auditLog"]} + expected = [{"id": "123456", + "serverAddress": "192.168.1.1", + "port": 514, + "protocol": "udp", + "components": [{"type": "auditLog"}]}] + + self._set_args(initial) + syslog = Syslog() + + with mock.patch(self.REQ_FUNC, side_effect=[(200, expected), (200, None)]): + updated = syslog.update_configuration() + self.assertFalse(updated) + + def test_update_configuration_record_partial_match_pass(self): + """Verify existing syslog server record partial match results in an update request.""" + initial = {"state": "present", + "ssid": "1", + "address": "192.168.1.1", + "port": "514", + "protocol": "tcp", + "components": ["auditLog"]} + expected = [{"id": "123456", + "serverAddress": "192.168.1.1", + "port": 514, + "protocol": "udp", + "components": [{"type": "auditLog"}]}] + + self._set_args(initial) + syslog = Syslog() + + with mock.patch(self.REQ_FUNC, side_effect=[(200, expected), (200, None)]): + updated = syslog.update_configuration() + self.assertTrue(updated) + + def test_update_configuration_record_no_match_pass(self): + """Verify existing syslog server record partial match results in an update request.""" + initial = {"state": "present", + "ssid": "1", + "address": "192.168.1.1", + "port": "514", + "protocol": "tcp", + "components": ["auditLog"]} + expected = [{"id": "123456", + "serverAddress": "192.168.1.100", + "port": 514, + "protocol": "udp", + "components": [{"type": "auditLog"}]}] + + self._set_args(initial) + syslog = Syslog() + + with mock.patch(self.REQ_FUNC, side_effect=[(200, expected), (200, dict(id=1234))]): + updated = syslog.update_configuration() + self.assertTrue(updated) + + def test_update_configuration_record_no_match_defaults_pass(self): + """Verify existing syslog server record partial match results in an update request.""" + initial = {"state": "present", + "ssid": "1", + "address": "192.168.1.1", + "port": "514", + "protocol": "tcp", + "components": ["auditLog"]} + expected = [{"id": "123456", + "serverAddress": "192.168.1.100", + "port": 514, + "protocol": "udp", + "components": [{"type": "auditLog"}]}] + + self._set_args(initial) + syslog = Syslog() + + with mock.patch(self.REQ_FUNC, side_effect=[(200, expected), (200, dict(id=1234))]): + updated = syslog.update_configuration() + self.assertTrue(updated)