Defined netapp_e_syslog storage module (#42421)
Module allows syslog server configuration on NetApp E-Series storage arrays.
This commit is contained in:
parent
feb212b0a1
commit
7eda94dc8d
2 changed files with 406 additions and 0 deletions
280
lib/ansible/modules/storage/netapp/netapp_e_syslog.py
Normal file
280
lib/ansible/modules/storage/netapp/netapp_e_syslog.py
Normal 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()
|
126
test/units/modules/storage/netapp/test_netapp_e_syslog.py
Normal file
126
test/units/modules/storage/netapp/test_netapp_e_syslog.py
Normal 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)
|
Loading…
Reference in a new issue