diff --git a/lib/ansible/modules/network/f5/bigip_qkview.py b/lib/ansible/modules/network/f5/bigip_qkview.py
new file mode 100644
index 0000000000..e129cbabf4
--- /dev/null
+++ b/lib/ansible/modules/network/f5/bigip_qkview.py
@@ -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 .
+
+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()
diff --git a/test/units/modules/network/f5/test_bigip_qkview.py b/test/units/modules/network/f5/test_bigip_qkview.py
new file mode 100644
index 0000000000..b9de94f562
--- /dev/null
+++ b/test/units/modules/network/f5/test_bigip_qkview.py
@@ -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 .
+
+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