Adds bigip_qkview module (#25980)

This module can be used to generate qkviews on a bigip and
save them to a location on the ansible controller.
This module is primarily used for diagnostics.

Unit tests are provided. Integration tests can be found here

https://github.com/F5Networks/f5-ansible/blob/devel/test/integration/bigip_qkview.yaml#L23
https://github.com/F5Networks/f5-ansible/tree/devel/test/integration/targets/bigip_qkview/tasks
This commit is contained in:
Tim Rupp 2017-06-28 09:31:17 -07:00 committed by John R Barker
parent 16bd93a14e
commit 04ac15f241
2 changed files with 656 additions and 0 deletions

View file

@ -0,0 +1,465 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright 2017 F5 Networks Inc.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
ANSIBLE_METADATA = {
'status': ['preview'],
'supported_by': 'community',
'metadata_version': '1.0'
}
DOCUMENTATION = '''
---
module: bigip_qkview
short_description: Manage qkviews on the device.
description:
- Manages creating and downloading qkviews from a BIG-IP. Various
options can be provided when creating qkviews. The qkview is important
when dealing with F5 support. It may be required that you upload this
qkview to the supported channels during resolution of an SRs that you
may have opened.
version_added: "2.4"
options:
filename:
description:
- Name of the qkview to create on the remote BIG-IP.
default: "localhost.localdomain.qkview"
dest:
description:
- Destination on your local filesystem when you want to save the qkview.
required: True
asm_request_log:
description:
- When C(True), includes the ASM request log data. When C(False),
excludes the ASM request log data.
default: no
choices:
- yes
- no
max_file_size:
description:
- Max file size, in bytes, of the qkview to create. By default, no max
file size is specified.
default: 0
complete_information:
description:
- Include complete information in the qkview.
default: yes
choices:
- yes
- no
exclude_core:
description:
- Exclude core files from the qkview.
default: no
choices:
- yes
- no
exclude:
description:
- Exclude various file from the qkview.
choices:
- all
- audit
- secure
- bash_history
force:
description:
- If C(no), the file will only be transferred if the destination does not
exist.
default: yes
choices:
- yes
- no
notes:
- Requires the f5-sdk Python package on the host. This is as easy as pip
install f5-sdk.
- This module does not include the "max time" or "restrict to blade" options.
requirements:
- f5-sdk >= 2.2.3
extends_documentation_fragment: f5
author:
- Tim Rupp (@caphrim007)
'''
EXAMPLES = '''
- name: Fetch a qkview from the remote device
bigip_qkview:
asm_request_log: "yes"
exclude:
- audit
- secure
dest: "/tmp/localhost.localdomain.qkview"
delegate_to: localhost
'''
RETURN = '''
stdout:
description: The set of responses from the commands
returned: always
type: list
sample: ['...', '...']
stdout_lines:
description: The value of stdout split into a list
returned: always
type: list
sample: [['...', '...'], ['...'], ['...']]
'''
import re
import os
from distutils.version import LooseVersion
from ansible.module_utils.six import string_types
from ansible.module_utils.f5_utils import (
AnsibleF5Client,
AnsibleF5Parameters,
HAS_F5SDK,
F5ModuleError,
iControlUnexpectedHTTPError
)
class Parameters(AnsibleF5Parameters):
api_attributes = [
'exclude', 'exclude_core', 'complete_information', 'max_file_size',
'asm_request_log', 'filename_cmd'
]
returnables = ['stdout', 'stdout_lines', 'warnings']
@property
def exclude(self):
if self._values['exclude'] is None:
return None
exclude = ' '.join(self._values['exclude'])
return "--exclude='{0}'".format(exclude)
@property
def exclude_raw(self):
return self._values['exclude']
@property
def exclude_core(self):
if self._values['exclude']:
return '-C'
else:
return None
@property
def complete_information(self):
if self._values['complete_information']:
return '-c'
return None
@property
def max_file_size(self):
if self._values['max_file_size'] in [None, 0]:
return '-s0'
return '-s {0}'.format(self._values['max_file_size'])
@property
def asm_request_log(self):
if self._values['asm_request_log']:
return '-o asm-request-log'
return None
@property
def filename(self):
pattern = r'^[\w\.]+$'
filename = os.path.basename(self._values['filename'])
if re.match(pattern, filename):
return filename
else:
raise F5ModuleError(
"The provided filename must contain word characters only."
)
@property
def filename_cmd(self):
return '-f {0}'.format(self.filename)
def to_return(self):
result = {}
try:
for returnable in self.returnables:
result[returnable] = getattr(self, returnable)
result = self._filter_params(result)
except Exception:
pass
return result
def api_params(self):
result = {}
for api_attribute in self.api_attributes:
if self.api_map is not None and api_attribute in self.api_map:
result[api_attribute] = getattr(self, self.api_map[api_attribute])
else:
result[api_attribute] = getattr(self, api_attribute)
result = self._filter_params(result)
return result
class ModuleManager(object):
def __init__(self, client):
self.client = client
def exec_module(self):
if self.is_version_less_than_14():
manager = self.get_manager('madm')
else:
manager = self.get_manager('bulk')
return manager.exec_module()
def get_manager(self, type):
if type == 'madm':
return MadmLocationManager(self.client)
elif type == 'bulk':
return BulkLocationManager(self.client)
def is_version_less_than_14(self):
"""Checks to see if the TMOS version is less than 14
Anything less than BIG-IP 13.x does not support users
on different partitions.
:return: Bool
"""
version = self.client.api.tmos_version
if LooseVersion(version) < LooseVersion('14.0.0'):
return True
else:
return False
class BaseManager(object):
def __init__(self, client):
self.client = client
self.want = Parameters(self.client.module.params)
self.changes = Parameters()
def _set_changed_options(self):
changed = {}
for key in Parameters.returnables:
if getattr(self.want, key) is not None:
changed[key] = getattr(self.want, key)
if changed:
self.changes = Parameters(changed)
def _to_lines(self, stdout):
lines = []
if isinstance(stdout, string_types):
lines = str(stdout).split('\n')
return lines
def exec_module(self):
result = dict()
try:
self.present()
except iControlUnexpectedHTTPError as e:
raise F5ModuleError(str(e))
result.update(**self.changes.to_return())
result.update(dict(changed=False))
return result
def present(self):
if os.path.exists(self.want.dest) and not self.want.force:
raise F5ModuleError(
"The specified 'dest' file already exists"
)
if self.want.exclude:
choices = ['all', 'audit', 'secure', 'bash_history']
if not all(x in choices for x in self.want.exclude_raw):
raise F5ModuleError(
"The specified excludes must be in the following list: "
"{0}".format(','.join(choices))
)
self.execute()
def exists(self):
ls = self.client.api.tm.util.unix_ls.exec_cmd(
'run', utilCmdArgs=self.remote_dir
)
# Empty directories return nothing to the commandResult
if not hasattr(ls, 'commandResult'):
return False
if self.want.filename in ls.commandResult:
return True
else:
return False
def execute(self):
response = self.execute_on_device()
result = self._move_qkview_to_download()
if not result:
raise F5ModuleError(
"Failed to move the file to a downloadable location"
)
self._download_file()
if not os.path.exists(self.want.dest):
raise F5ModuleError(
"Failed to save the qkview to local disk"
)
self._delete_qkview()
result = self.exists()
if result:
raise F5ModuleError(
"Failed to remove the remote qkview"
)
self.changes = Parameters({
'stdout': response,
'stdout_lines': self._to_lines(response)
})
def _delete_qkview(self):
tpath_name = '{0}/{1}'.format(self.remote_dir, self.want.filename)
self.client.api.tm.util.unix_rm.exec_cmd(
'run', utilCmdArgs=tpath_name
)
def execute_on_device(self):
params = self.want.api_params().values()
output = self.client.api.tm.util.qkview.exec_cmd(
'run',
utilCmdArgs='{0}'.format(' '.join(params))
)
if hasattr(output, 'commandResult'):
return str(output.commandResult)
return None
class BulkLocationManager(BaseManager):
def __init__(self, client):
super(BulkLocationManager, self).__init__(client)
self.remote_dir = '/var/config/rest/bulk'
def _move_qkview_to_download(self):
try:
move_path = '/var/tmp/{0} {1}/{0}'.format(
self.want.filename, self.remote_dir
)
self.client.api.tm.util.unix_mv.exec_cmd(
'run',
utilCmdArgs=move_path
)
return True
except Exception:
return False
def _download_file(self):
bulk = self.client.api.shared.file_transfer.bulk
bulk.download_file(self.want.filename, self.want.dest)
if os.path.exists(self.want.dest):
return True
return False
class MadmLocationManager(BaseManager):
def __init__(self, client):
super(MadmLocationManager, self).__init__(client)
self.remote_dir = '/var/config/rest/madm'
def _move_qkview_to_download(self):
try:
move_path = '/var/tmp/{0} {1}/{0}'.format(
self.want.filename, self.remote_dir
)
self.client.api.tm.util.unix_mv.exec_cmd(
'run',
utilCmdArgs=move_path
)
return True
except Exception:
return False
def _download_file(self):
madm = self.client.api.shared.file_transfer.madm
madm.download_file(self.want.filename, self.want.dest)
if os.path.exists(self.want.dest):
return True
return False
class ArgumentSpec(object):
def __init__(self):
self.supports_check_mode = True
self.argument_spec = dict(
filename=dict(
default='localhost.localdomain.qkview'
),
asm_request_log=dict(
type='bool',
default=False,
),
max_file_size=dict(
type='int',
),
complete_information=dict(
default=False,
type='bool'
),
exclude_core=dict(
default=False,
type='bool'
),
force=dict(
default=True,
type='bool'
),
exclude=dict(
type='list'
),
dest=dict(
type='path',
required=True
)
)
self.f5_product_name = 'bigip'
def main():
if not HAS_F5SDK:
raise F5ModuleError("The python f5-sdk module is required")
spec = ArgumentSpec()
client = AnsibleF5Client(
argument_spec=spec.argument_spec,
supports_check_mode=spec.supports_check_mode,
f5_product_name=spec.f5_product_name
)
try:
mm = ModuleManager(client)
results = mm.exec_module()
client.module.exit_json(**results)
except F5ModuleError as e:
client.module.fail_json(msg=str(e))
if __name__ == '__main__':
main()

View file

@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
#
# Copyright 2017 F5 Networks Inc.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import json
import sys
from nose.plugins.skip import SkipTest
if sys.version_info < (2, 7):
raise SkipTest("F5 Ansible modules require Python >= 2.7")
from ansible.compat.tests import unittest
from ansible.compat.tests.mock import patch, Mock
from ansible.module_utils import basic
from ansible.module_utils._text import to_bytes
from ansible.module_utils.f5_utils import AnsibleF5Client
try:
from library.bigip_qkview import Parameters
from library.bigip_qkview import ModuleManager
from library.bigip_qkview import MadmLocationManager
from library.bigip_qkview import BulkLocationManager
from library.bigip_qkview import ArgumentSpec
except ImportError:
try:
from ansible.modules.network.f5.bigip_qkview import Parameters
from ansible.modules.network.f5.bigip_qkview import ModuleManager
from ansible.modules.network.f5.bigip_qkview import MadmLocationManager
from ansible.modules.network.f5.bigip_qkview import BulkLocationManager
from ansible.modules.network.f5.bigip_qkview import ArgumentSpec
except ImportError:
raise SkipTest("F5 Ansible modules require the f5-sdk Python library")
fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
fixture_data = {}
def set_module_args(args):
args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
basic._ANSIBLE_ARGS = to_bytes(args)
def load_fixture(name):
path = os.path.join(fixture_path, name)
if path in fixture_data:
return fixture_data[path]
with open(path) as f:
data = f.read()
try:
data = json.loads(data)
except Exception:
pass
fixture_data[path] = data
return data
class TestParameters(unittest.TestCase):
def test_module_parameters(self):
args = dict(
filename='foo.qkview',
asm_request_log=False,
max_file_size=1024,
complete_information=True,
exclude_core=True,
force=False,
exclude=['audit', 'secure'],
dest='/tmp/foo.qkview'
)
p = Parameters(args)
assert p.filename == 'foo.qkview'
assert p.asm_request_log is None
assert p.max_file_size == '-s 1024'
assert p.complete_information == '-c'
assert p.exclude_core == '-C'
assert p.force is False
assert len(p.exclude_core) == 2
assert 'audit' in p.exclude
assert 'secure' in p.exclude
assert p.dest == '/tmp/foo.qkview'
def test_module_asm_parameter(self):
args = dict(
asm_request_log=True,
)
p = Parameters(args)
assert p.asm_request_log == '-o asm-request-log'
@patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root',
return_value=True)
class TestMadmLocationManager(unittest.TestCase):
def setUp(self):
self.spec = ArgumentSpec()
def test_create_qkview_default_options(self, *args):
set_module_args(dict(
dest='/tmp/foo.qkview',
server='localhost',
user='admin',
password='password'
))
client = AnsibleF5Client(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
f5_product_name=self.spec.f5_product_name
)
# Override methods in the specific type of manager
tm = MadmLocationManager(client)
tm.exists = Mock(return_value=False)
tm.execute_on_device = Mock(return_value=True)
tm._move_qkview_to_download = Mock(return_value=True)
tm._download_file = Mock(return_value=True)
tm._delete_qkview = Mock(return_value=True)
# Override methods to force specific logic in the module to happen
mm = ModuleManager(client)
mm.is_version_less_than_14 = Mock(return_value=True)
mm.get_manager = Mock(return_value=tm)
with patch('os.path.exists') as mo:
mo.return_value = True
results = mm.exec_module()
assert results['changed'] is False
@patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root',
return_value=True)
class TestBulkLocationManager(unittest.TestCase):
def setUp(self):
self.spec = ArgumentSpec()
def test_create_qkview_default_options(self, *args):
set_module_args(dict(
dest='/tmp/foo.qkview',
server='localhost',
user='admin',
password='password'
))
client = AnsibleF5Client(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
f5_product_name=self.spec.f5_product_name
)
# Override methods in the specific type of manager
tm = BulkLocationManager(client)
tm.exists = Mock(return_value=False)
tm.execute_on_device = Mock(return_value=True)
tm._move_qkview_to_download = Mock(return_value=True)
tm._download_file = Mock(return_value=True)
tm._delete_qkview = Mock(return_value=True)
# Override methods to force specific logic in the module to happen
mm = ModuleManager(client)
mm.is_version_less_than_14 = Mock(return_value=False)
mm.get_manager = Mock(return_value=tm)
with patch('os.path.exists') as mo:
mo.return_value = True
results = mm.exec_module()
assert results['changed'] is False