Defined netapp_e_syslog storage module (#42421)

Module allows syslog server configuration on NetApp E-Series storage arrays.
This commit is contained in:
ndswartz 2018-08-28 07:22:36 -05:00 committed by John R Barker
parent feb212b0a1
commit 7eda94dc8d
2 changed files with 406 additions and 0 deletions

View file

@ -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()

View file

@ -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)