#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright: (c) 2018, 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_appsvcs_extension short_description: Manage application service deployments description: - Manages application service deployments via the App Services Extension functionality in BIG-IP. version_added: 2.7 options: content: description: - Declaration of tenants configured on the system. - This parameter is most often used along with the C(file) or C(template) lookup plugins. Refer to the examples section for correct usage. - For anything advanced or with formatting consider using the C(template) lookup. - This can additionally be used for specifying application service configurations directly in YAML, however that is not an encouraged practice and, if used at all, should only be used for the absolute smallest of configurations to prevent your Playbooks from becoming too large. - If you C(content) includes encrypted values (such as ciphertexts, passphrases, etc), the returned C(changed) value will always be true. - If you are using the C(to_nice_json) filter, it will cause this module to fail because the purpose of that filter is to format the JSON to be human-readable and this process includes inserting "extra characters that break JSON validators. required: True tenants: description: - A list of tenants that you want to remove. - This parameter is only relevant when C(state) is C(absent). It will be ignored when C(state) is C(present). - A value of C(all) will remove all tenants. - Tenants can be specified as a list as well to remove only specific tenants. force: description: - Force updates a declaration. - This parameter should be used in cases where your declaration includes items that are encrypted or in cases (such as WAF Policies) where you want a large reload to take place. type: bool default: no state: description: - When C(state) is C(present), ensures the configuration exists. - When C(state) is C(absent), ensures that the configuration is removed. choices: - present - absent default: present extends_documentation_fragment: f5 author: - Tim Rupp (@caphrim007) ''' EXAMPLES = r''' - name: Deploy an app service configuration bigip_appsvcs_extension: content: "{{ lookup('file', '/path/to/appsvcs.json') }}" state: present provider: password: secret server: lb.mydomain.com user: admin delegate_to: localhost - name: Remove all app service configurations bigip_appsvcs_extension: tenants: all state: absent provider: password: secret server: lb.mydomain.com user: admin delegate_to: localhost - name: Remove tenants T1 and T2 from app service configurations bigip_appsvcs_extension: tenants: - T1 - T2 state: absent provider: password: secret server: lb.mydomain.com user: admin delegate_to: localhost ''' RETURN = r''' action: description: - The action performed. returned: changed type: string sample: deploy ''' from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.six import iteritems from ansible.module_utils.six import string_types 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 fq_name from library.module_utils.network.f5.common import f5_argument_spec 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 fq_name from ansible.module_utils.network.f5.common import f5_argument_spec try: import json except ImportError: import simplejson as json class Parameters(AnsibleF5Parameters): api_map = { 'class': 'class_name', 'patchBody': 'patch_body', 'resourceTimeout': 'resource_timeout', 'targetTimeout': 'target_timeout', 'targetTokens': 'target_tokens', 'targetPassphrase': 'target_passphrase', 'targetUsername': 'target_username', 'targetPort': 'target_port', 'targetHost': 'target_host', 'retrieveAge': 'retrieve_age', 'logLevel': 'log_level', 'historyLimit': 'history_limit', 'syncToGroup': 'sync_to_group', 'redeployUpdateMode': 'redeploy_update_mode', 'redeployAge': 'redeploy_age', } api_attributes = [ 'class', 'action', 'persist', 'declaration', 'patchBody', 'resourceTimeout', 'targetTimeout', 'targetTokens', 'targetPassphrase', 'targetUsername', 'targetPort', 'targetHost', 'retrieveAge', 'trace', 'logLevel', 'historyLimit', 'syncToGroup', 'redeployUpdateMode', 'redeployAge', ] returnables = [ 'class_name', 'action', 'persist', 'declaration', 'patch_body', 'resource_timeout', 'target_timeout', 'target_tokens', 'target_passphrase', 'target_username', 'target_port', 'target_host', 'retrieve_age', 'trace', 'log_level', 'history_limit', 'sync_to_group', 'redeploy_update_mode', 'redeploy_age', ] updatables = [ 'class_name', 'action', 'persist', 'declaration', 'patch_body', 'resource_timeout', 'target_timeout', 'target_tokens', 'target_passphrase', 'target_username', 'target_port', 'target_host', 'retrieve_age', 'trace', 'log_level', 'history_limit', 'sync_to_group', 'redeploy_update_mode', 'redeploy_age', ] class ApiParameters(Parameters): pass class ModuleParameters(Parameters): @property def content(self): if self._values['content'] is None: return None if isinstance(self._values['content'], string_types): return json.loads(self._values['content'] or 'null') else: return self._values['content'] @property def class_name(self): return self._values['content'].get('class', None) @property def action(self): return self._values['content'].get('action', None) @property def declaration(self): return self._values['content'].get('declaration', None) @property def persist(self): if self._values['content']: return self._values['content'].get('persist', None) elif self.param_persist: return self.param_persist return None @property def param_persist(self): if self._values['parameters'] is None: return None result = self._values['parameters'].get('persist', None) return result @property def tenants(self): if self._values['tenants'] in [None, 'all']: return '' if isinstance(self._values['tenants'], list): return ','.join(self._values['tenants']) class Changes(Parameters): 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 class UsableChanges(Changes): pass class ReportableChanges(Changes): @property def declaration(self): return None @property def target_passphrase(self): return None @property def class_name(self): return None @property def persist(self): return None class Difference(object): def __init__(self, want, have=None): self.want = want self.have = have def compare(self, param): try: result = getattr(self, param) return result except AttributeError: return self.__default(param) def __default(self, param): attr1 = getattr(self.want, param) try: attr2 = getattr(self.have, param) if attr1 != attr2: return attr1 except AttributeError: return attr1 class ModuleManager(object): def __init__(self, *args, **kwargs): self.module = kwargs.get('module', None) self.client = kwargs.get('client', None) self.want = ModuleParameters(params=self.module.params) self.have = ApiParameters() self.changes = UsableChanges() 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 = UsableChanges(params=changed) def exec_module(self): changed = False result = dict() state = self.want.state if state == "present": changed = self.present() elif state == "absent": changed = self.absent() reportable = ReportableChanges(params=self.changes.to_return()) changes = reportable.to_return() result.update(**changes) result.update(dict(changed=changed)) return result def present(self): if self.exists(): return False return self.upsert() def remove(self): if self.module.check_mode: return True self.remove_from_device() if self.tenant_exists(): raise F5ModuleError("Failed to delete the resource.") return True def upsert(self): self._set_changed_options() if self.module.check_mode: return True if self.want.content == 'all': raise F5ModuleError( "'all' keyword cannot be used when 'state' is 'present'." ) self.upsert_on_device() return True def _get_errors_from_response(self, messages): results = [] if 'results' not in messages: if 'message' in messages: results.append(messages['message']) if 'errors' in messages: results += messages['errors'] else: for message in messages['results']: if 'message' in message and message['message'] == 'declaration failed': results.append(message['response']) if 'errors' in message: results += message['errors'] return results def upsert_on_device(self): uri = 'https://{0}:{1}/mgmt/shared/appsvcs/declare/'.format( self.client.provider['server'], self.client.provider['server_port'], ) resp = self.client.api.post(uri, json=self.want.content) if resp.status != 200: result = resp.json() errors = self._get_errors_from_response(result) if errors: message = "{0}".format('. '.join(errors)) raise F5ModuleError(message) raise F5ModuleError(resp.content) else: result = resp.json() errors = self._get_errors_from_response(result) if errors: message = "{0}".format('. '.join(errors)) raise F5ModuleError(message) def ignore_changes(self, obj): if isinstance(obj, dict): if 'passphrase' in obj: obj['passphrase']['ignoreChanges'] = True if 'class' in obj and obj['class'] == 'WAF_Policy': obj['ignoreChanges'] = True return dict((k, self.ignore_changes(v)) for k, v in iteritems(obj)) else: return obj def exists(self): declaration = {} if self.want.content is None: raise F5ModuleError( "Empty content cannot be specified when 'state' is 'present'." ) try: declaration.update(self.want.content) except ValueError: raise F5ModuleError( "The provided 'content' could not be converted into valid json. If you " "are using the 'to_nice_json' filter, please remove it." ) declaration['action'] = 'dry-run' # This deals with cases where you're comparing a passphrase. # # Passphrases will always cause an idepotent operation to register # a change. Therefore, by specifying "force", you are instructing # the module to **not** ignore the passphrase. # # This will cause the module to not append ignore clauses to the # classes that support them. if not self.want.force: self.ignore_changes(declaration) uri = "https://{0}:{1}/mgmt/shared/appsvcs/declare/".format( self.client.provider['server'], self.client.provider['server_port'], ) resp = self.client.api.post(uri, json=declaration) try: response = resp.json() except ValueError: return False try: if response['results'][0]['message'] == 'no change': return True except KeyError: return False def tenant_exists(self): uri = "https://{0}:{1}/mgmt/shared/appsvcs/declare/{2}".format( self.client.provider['server'], self.client.provider['server_port'], self.want.tenants ) resp = self.client.api.get(uri) try: response = resp.json() except ValueError: return False if resp.status == 404 or 'statusCode' in response and response['statusCode'] == 404: return False return True def absent(self): if self.tenant_exists(): return self.remove() return False def remove_from_device(self): uri = 'https://{0}:{1}/mgmt/shared/appsvcs/declare/{2}'.format( self.client.provider['server'], self.client.provider['server_port'], self.want.tenants ) response = self.client.api.delete(uri) if response.status == 200: return True raise F5ModuleError(response.content) class ArgumentSpec(object): def __init__(self): self.supports_check_mode = True argument_spec = dict( content=dict(type='raw'), state=dict( default='present', choices=['present', 'absent'] ), tenants=dict(type='raw'), force=dict(type='bool', default='no') ) self.argument_spec = {} self.argument_spec.update(f5_argument_spec) self.argument_spec.update(argument_spec) self.required_if = [ ['state', 'present', ['content']] ] self.mutually_exclusive = [ ['content', 'tenants'] ] def main(): spec = ArgumentSpec() module = AnsibleModule( argument_spec=spec.argument_spec, supports_check_mode=spec.supports_check_mode, required_if=spec.required_if ) client = F5RestClient(**module.params) try: mm = ModuleManager(module=module, client=client) results = mm.exec_module() cleanup_tokens(client) module.exit_json(**results) except F5ModuleError as ex: cleanup_tokens(client) module.fail_json(msg=str(ex)) if __name__ == '__main__': main()