612 lines
19 KiB
Python
612 lines
19 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright: (c) 2017, F5 Networks 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': 'certified'}
|
|
|
|
DOCUMENTATION = r'''
|
|
---
|
|
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
|
|
type: bool
|
|
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: no
|
|
type: bool
|
|
exclude_core:
|
|
description:
|
|
- Exclude core files from the qkview.
|
|
default: no
|
|
type: bool
|
|
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
|
|
type: bool
|
|
notes:
|
|
- This module does not include the "max time" or "restrict to blade" options.
|
|
- If you are using this module with either Ansible Tower or Ansible AWX, you
|
|
should be aware of how these Ansible products execute jobs in restricted
|
|
environments. More informat can be found here
|
|
https://clouddocs.f5.com/products/orchestration/ansible/devel/usage/module-usage-with-tower.html
|
|
extends_documentation_fragment: f5
|
|
author:
|
|
- Tim Rupp (@caphrim007)
|
|
'''
|
|
|
|
EXAMPLES = r'''
|
|
- name: Fetch a qkview from the remote device
|
|
bigip_qkview:
|
|
asm_request_log: yes
|
|
exclude:
|
|
- audit
|
|
- secure
|
|
dest: /tmp/localhost.localdomain.qkview
|
|
provider:
|
|
password: secret
|
|
server: lb.mydomain.com
|
|
user: admin
|
|
delegate_to: localhost
|
|
'''
|
|
|
|
RETURN = r'''
|
|
# only common fields returned
|
|
'''
|
|
|
|
import os
|
|
import re
|
|
import socket
|
|
import ssl
|
|
import time
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
from distutils.version import LooseVersion
|
|
|
|
try:
|
|
import urlparse
|
|
except ImportError:
|
|
import urllib.parse as urlparse
|
|
|
|
try:
|
|
from library.module_utils.network.f5.bigip import F5RestClient
|
|
from library.module_utils.network.f5.common import F5ModuleError
|
|
from library.module_utils.network.f5.common import AnsibleF5Parameters
|
|
from library.module_utils.network.f5.common import cleanup_tokens
|
|
from library.module_utils.network.f5.common import f5_argument_spec
|
|
from library.module_utils.network.f5.common import exit_json
|
|
from library.module_utils.network.f5.common import fail_json
|
|
from library.module_utils.network.f5.common import transform_name
|
|
from library.module_utils.network.f5.icontrol import download_file
|
|
except ImportError:
|
|
from ansible.module_utils.network.f5.bigip import F5RestClient
|
|
from ansible.module_utils.network.f5.common import F5ModuleError
|
|
from ansible.module_utils.network.f5.common import AnsibleF5Parameters
|
|
from ansible.module_utils.network.f5.common import cleanup_tokens
|
|
from ansible.module_utils.network.f5.common import f5_argument_spec
|
|
from ansible.module_utils.network.f5.common import exit_json
|
|
from ansible.module_utils.network.f5.common import fail_json
|
|
from ansible.module_utils.network.f5.common import transform_name
|
|
from ansible.module_utils.network.f5.icontrol import download_file
|
|
|
|
|
|
class Parameters(AnsibleF5Parameters):
|
|
api_attributes = [
|
|
'asm_request_log',
|
|
'complete_information',
|
|
'exclude',
|
|
'exclude_core',
|
|
'filename_cmd',
|
|
'max_file_size',
|
|
]
|
|
|
|
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]:
|
|
return None
|
|
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, *args, **kwargs):
|
|
self.module = kwargs.get('module', None)
|
|
self.client = kwargs.get('client', None)
|
|
self.kwargs = kwargs
|
|
|
|
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.kwargs)
|
|
elif type == 'bulk':
|
|
return BulkLocationManager(**self.kwargs)
|
|
|
|
def is_version_less_than_14(self):
|
|
uri = "https://{0}:{1}/mgmt/tm/sys".format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port'],
|
|
)
|
|
resp = self.client.api.get(uri)
|
|
try:
|
|
response = resp.json()
|
|
except ValueError as ex:
|
|
raise F5ModuleError(str(ex))
|
|
version = urlparse.parse_qs(urlparse.urlparse(response['selfLink']).query)['ver'][0]
|
|
if LooseVersion(version) < LooseVersion('14.0.0'):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
class BaseManager(object):
|
|
def __init__(self, *args, **kwargs):
|
|
self.module = kwargs.get('module', None)
|
|
self.client = kwargs.get('client', None)
|
|
self.have = None
|
|
self.want = Parameters(params=self.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(params=changed)
|
|
|
|
def exec_module(self):
|
|
result = dict()
|
|
|
|
self.present()
|
|
|
|
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 not os.path.exists(os.path.dirname(self.want.dest)):
|
|
raise F5ModuleError(
|
|
"The directory of your 'dest' file does not exist."
|
|
)
|
|
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):
|
|
params = dict(
|
|
command='run',
|
|
utilCmdArgs=self.remote_dir
|
|
)
|
|
uri = "https://{0}:{1}/mgmt/tm/util/unix-ls".format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port'],
|
|
)
|
|
resp = self.client.api.post(uri, json=params)
|
|
try:
|
|
response = resp.json()
|
|
except ValueError:
|
|
return False
|
|
if resp.status == 404 or 'code' in response and response['code'] == 404:
|
|
return False
|
|
|
|
try:
|
|
if self.want.filename in response['commandResult']:
|
|
return True
|
|
except KeyError:
|
|
return False
|
|
|
|
def execute(self):
|
|
response = self.execute_on_device()
|
|
if not response:
|
|
raise F5ModuleError(
|
|
"Failed to create qkview 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"
|
|
)
|
|
|
|
def _delete_qkview(self):
|
|
tpath_name = '{0}/{1}'.format(self.remote_dir, self.want.filename)
|
|
params = dict(
|
|
command='run',
|
|
utilCmdArgs=tpath_name
|
|
)
|
|
uri = "https://{0}:{1}/mgmt/tm/util/unix-rm".format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port'],
|
|
)
|
|
resp = self.client.api.post(uri, json=params)
|
|
try:
|
|
response = resp.json()
|
|
except ValueError:
|
|
return False
|
|
if resp.status == 404 or 'code' in response and response['code'] == 404:
|
|
return False
|
|
|
|
def execute_on_device(self):
|
|
self._upsert_temporary_cli_script_on_device()
|
|
task_id = self._create_async_task_on_device()
|
|
self._exec_async_task_on_device(task_id)
|
|
self._wait_for_async_task_to_finish_on_device(task_id)
|
|
self._remove_temporary_cli_script_from_device()
|
|
return True
|
|
|
|
def _upsert_temporary_cli_script_on_device(self):
|
|
args = {
|
|
"name": "__ansible_mkqkview",
|
|
"apiAnonymous": """
|
|
proc script::run {} {
|
|
set cmd [lreplace $tmsh::argv 0 0]; eval "exec $cmd 2> /dev/null"
|
|
}
|
|
"""
|
|
}
|
|
result = self._create_temporary_cli_script_on_device(args)
|
|
if result:
|
|
return True
|
|
return self._update_temporary_cli_script_on_device(args)
|
|
|
|
def _create_temporary_cli_script_on_device(self, args):
|
|
uri = "https://{0}:{1}/mgmt/tm/cli/script".format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port'],
|
|
)
|
|
resp = self.client.api.post(uri, json=args)
|
|
try:
|
|
response = resp.json()
|
|
if 'code' in response and response['code'] in [404, 409]:
|
|
return False
|
|
except ValueError:
|
|
pass
|
|
if resp.status in [404, 409]:
|
|
return False
|
|
return True
|
|
|
|
def _update_temporary_cli_script_on_device(self, args):
|
|
uri = "https://{0}:{1}/mgmt/tm/cli/script/{2}".format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port'],
|
|
transform_name('Common', '__ansible_mkqkview')
|
|
)
|
|
resp = self.client.api.put(uri, json=args)
|
|
try:
|
|
resp.json()
|
|
return True
|
|
except ValueError:
|
|
raise F5ModuleError(
|
|
"Failed to update temporary cli script on device."
|
|
)
|
|
|
|
def _create_async_task_on_device(self):
|
|
"""Creates an async cli script task in the REST API
|
|
|
|
Returns:
|
|
int: The ID of the task staged for running.
|
|
|
|
:return:
|
|
"""
|
|
command = ' '.join(self.want.api_params().values())
|
|
args = {
|
|
"command": "run",
|
|
"name": "__ansible_mkqkview",
|
|
"utilCmdArgs": "/usr/bin/qkview {0}".format(command)
|
|
}
|
|
uri = "https://{0}:{1}/mgmt/tm/task/cli/script".format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port'],
|
|
)
|
|
resp = self.client.api.post(uri, json=args)
|
|
try:
|
|
response = resp.json()
|
|
return response['_taskId']
|
|
except ValueError:
|
|
raise F5ModuleError(
|
|
"Failed to create the async task on the device."
|
|
)
|
|
|
|
def _exec_async_task_on_device(self, task_id):
|
|
args = {"_taskState": "VALIDATING"}
|
|
uri = "https://{0}:{1}/mgmt/tm/task/cli/script/{2}".format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port'],
|
|
task_id
|
|
)
|
|
resp = self.client.api.put(uri, json=args)
|
|
try:
|
|
resp.json()
|
|
return True
|
|
except ValueError:
|
|
raise F5ModuleError(
|
|
"Failed to execute the async task on the device"
|
|
)
|
|
|
|
def _wait_for_async_task_to_finish_on_device(self, task_id):
|
|
uri = "https://{0}:{1}/mgmt/tm/task/cli/script/{2}/result".format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port'],
|
|
task_id
|
|
)
|
|
while True:
|
|
try:
|
|
resp = self.client.api.get(uri, timeout=10)
|
|
except (socket.timeout, ssl.SSLError):
|
|
continue
|
|
try:
|
|
response = resp.json()
|
|
except ValueError:
|
|
# It is possible that the API call can return invalid JSON.
|
|
# This invalid JSON appears to be just empty strings.
|
|
continue
|
|
if response['_taskState'] == 'FAILED':
|
|
raise F5ModuleError(
|
|
"qkview creation task failed unexpectedly."
|
|
)
|
|
if response['_taskState'] == 'COMPLETED':
|
|
return True
|
|
time.sleep(3)
|
|
|
|
def _remove_temporary_cli_script_from_device(self):
|
|
uri = "https://{0}:{1}/mgmt/tm/task/cli/script/{2}".format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port'],
|
|
transform_name('Common', '__ansible_mkqkview')
|
|
)
|
|
try:
|
|
self.client.api.delete(uri)
|
|
return True
|
|
except ValueError:
|
|
raise F5ModuleError(
|
|
"Failed to remove the temporary cli script from the device."
|
|
)
|
|
|
|
def _move_qkview_to_download(self):
|
|
uri = "https://{0}:{1}/mgmt/tm/util/unix-mv/".format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port']
|
|
)
|
|
args = dict(
|
|
command='run',
|
|
utilCmdArgs='/var/tmp/{0} {1}/{0}'.format(self.want.filename, self.remote_dir)
|
|
)
|
|
self.client.api.post(uri, json=args)
|
|
return True
|
|
|
|
|
|
class BulkLocationManager(BaseManager):
|
|
def __init__(self, *args, **kwargs):
|
|
super(BulkLocationManager, self).__init__(**kwargs)
|
|
self.remote_dir = '/var/config/rest/bulk'
|
|
|
|
def _download_file(self):
|
|
uri = "https://{0}:{1}/mgmt/shared/file-transfer/bulk/{2}".format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port'],
|
|
self.want.filename
|
|
)
|
|
download_file(self.client, uri, self.want.dest)
|
|
if os.path.exists(self.want.dest):
|
|
return True
|
|
return False
|
|
|
|
|
|
class MadmLocationManager(BaseManager):
|
|
def __init__(self, *args, **kwargs):
|
|
super(MadmLocationManager, self).__init__(**kwargs)
|
|
self.remote_dir = '/var/config/rest/madm'
|
|
|
|
def _download_file(self):
|
|
uri = "https://{0}:{1}/mgmt/shared/file-transfer/madm/{2}".format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port'],
|
|
self.want.filename
|
|
)
|
|
download_file(self.client, uri, 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
|
|
argument_spec = dict(
|
|
filename=dict(
|
|
default='localhost.localdomain.qkview'
|
|
),
|
|
asm_request_log=dict(
|
|
type='bool',
|
|
default='no',
|
|
),
|
|
max_file_size=dict(
|
|
type='int',
|
|
),
|
|
complete_information=dict(
|
|
default='no',
|
|
type='bool'
|
|
),
|
|
exclude_core=dict(
|
|
default="no",
|
|
type='bool'
|
|
),
|
|
force=dict(
|
|
default=True,
|
|
type='bool'
|
|
),
|
|
exclude=dict(
|
|
type='list',
|
|
choices=[
|
|
'all', 'audit', 'secure', 'bash_history'
|
|
]
|
|
),
|
|
dest=dict(
|
|
type='path',
|
|
required=True
|
|
)
|
|
)
|
|
self.argument_spec = {}
|
|
self.argument_spec.update(f5_argument_spec)
|
|
self.argument_spec.update(argument_spec)
|
|
|
|
|
|
def main():
|
|
spec = ArgumentSpec()
|
|
|
|
module = AnsibleModule(
|
|
argument_spec=spec.argument_spec,
|
|
supports_check_mode=spec.supports_check_mode,
|
|
)
|
|
|
|
client = F5RestClient(**module.params)
|
|
|
|
try:
|
|
mm = ModuleManager(module=module, client=client)
|
|
results = mm.exec_module()
|
|
cleanup_tokens(client)
|
|
exit_json(module, results, client)
|
|
except F5ModuleError as ex:
|
|
cleanup_tokens(client)
|
|
fail_json(module, ex, client)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|