From 59d470ae84da5ad57b933d34ed045e3935195ee3 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Sat, 17 Jun 2017 04:47:13 -0700 Subject: [PATCH] Adds bigip_iapp_service module (#25808) This module allows a user to manage the iApp services on a bigip that they created using the bigip_iapp_template module. Unit tests are provided. Integration tests can be found here https://github.com/F5Networks/f5-ansible/blob/devel/test/integration/bigip_iapp_service.yaml#L23 https://github.com/F5Networks/f5-ansible/tree/devel/test/integration/targets/bigip_iapp_service/tasks --- .../modules/network/f5/bigip_iapp_service.py | 472 ++++++++++++++++++ ...reate_iapp_service_parameters_f5_http.json | 195 ++++++++ ...pdate_iapp_service_parameters_f5_http.json | 195 ++++++++ .../network/f5/test_bigip_iapp_service.py | 264 ++++++++++ 4 files changed, 1126 insertions(+) create mode 100644 lib/ansible/modules/network/f5/bigip_iapp_service.py create mode 100644 test/units/modules/network/f5/fixtures/create_iapp_service_parameters_f5_http.json create mode 100644 test/units/modules/network/f5/fixtures/update_iapp_service_parameters_f5_http.json create mode 100644 test/units/modules/network/f5/test_bigip_iapp_service.py diff --git a/lib/ansible/modules/network/f5/bigip_iapp_service.py b/lib/ansible/modules/network/f5/bigip_iapp_service.py new file mode 100644 index 0000000000..4529000967 --- /dev/null +++ b/lib/ansible/modules/network/f5/bigip_iapp_service.py @@ -0,0 +1,472 @@ +#!/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_iapp_service +short_description: Manages TCL iApp services on a BIG-IP. +description: + - Manages TCL iApp services on a BIG-IP. +version_added: "2.4" +options: + name: + description: + - The name of the iApp service that you want to deploy. + required: True + template: + description: + - The iApp template from which to instantiate a new service. This + template must exist on your BIG-IP before you can successfully + create a service. This parameter is required if the C(state) + parameter is C(present). + parameters: + description: + - A hash of all the required template variables for the iApp template. + If your parameters are stored in a file (the more common scenario) + it is recommended you use either the `file` or `template` lookups + to supply the expected parameters. + force: + description: + - Forces the updating of an iApp service even if the parameters to the + service have not changed. This option is of particular importance if + the iApp template that underlies the service has been updated in-place. + This option is equivalent to re-configuring the iApp if that template + has changed. + default: False + state: + description: + - When C(present), ensures that the iApp service is created and running. + When C(absent), ensures that the iApp service has been removed. + default: present + choices: + - present + - absent +notes: + - Requires the f5-sdk Python package on the host. This is as easy as pip + install f5-sdk. + - Requires the deepdiff Python package on the host. This is as easy as pip + install f5-sdk. +requirements: + - f5-sdk + - deepdiff +extends_documentation_fragment: f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Create HTTP iApp service from iApp template + bigip_iapp_service: + name: "foo-service" + template: "f5.http" + parameters: "{{ lookup('file', 'f5.http.parameters.json') }}" + password: "secret" + server: "lb.mydomain.com" + state: "present" + user: "admin" + delegate_to: localhost + +- name: Upgrade foo-service to v1.2.0rc4 of the f5.http template + bigip_iapp_service: + name: "foo-service" + template: "f5.http.v1.2.0rc4" + password: "secret" + server: "lb.mydomain.com" + state: "present" + user: "admin" + delegate_to: localhost + +- name: Configure a service using parameters in YAML + bigip_iapp_service: + name: "tests" + template: "web_frontends" + password: "admin" + server: "{{ inventory_hostname }}" + server_port: "{{ bigip_port }}" + validate_certs: "{{ validate_certs }}" + state: "present" + user: "admin" + parameters: + variables: + - name: "var__vs_address" + value: "1.1.1.1" + - name: "pm__apache_servers_for_http" + value: "2.2.2.1:80" + - name: "pm__apache_servers_for_https" + value: "2.2.2.2:80" + delegate_to: localhost + +- name: Re-configure a service whose underlying iApp was updated in place + bigip_iapp_service: + name: "tests" + template: "web_frontends" + password: "admin" + force: yes + server: "{{ inventory_hostname }}" + server_port: "{{ bigip_port }}" + validate_certs: "{{ validate_certs }}" + state: "present" + user: "admin" + parameters: + variables: + - name: "var__vs_address" + value: "1.1.1.1" + - name: "pm__apache_servers_for_http" + value: "2.2.2.1:80" + - name: "pm__apache_servers_for_https" + value: "2.2.2.2:80" + delegate_to: localhost +''' + +RETURN = ''' +# only common fields returned +''' + +from ansible.module_utils.f5_utils import ( + AnsibleF5Client, + AnsibleF5Parameters, + HAS_F5SDK, + F5ModuleError, + iteritems, + iControlUnexpectedHTTPError +) +from deepdiff import DeepDiff + + +class Parameters(AnsibleF5Parameters): + returnables = [] + api_attributes = [ + 'tables', 'variables', 'template', 'lists' + ] + updatables = ['tables', 'variables', 'lists'] + + def to_return(self): + result = {} + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + 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 + + @property + def tables(self): + result = [] + if not self._values['tables']: + return None + tables = self._values['tables'] + for table in tables: + tmp = dict() + name = table.get('name', None) + if name is None: + raise F5ModuleError( + "One of the provided tables does not have a name" + ) + tmp['name'] = str(name) + columns = table.get('columnNames', None) + if columns: + tmp['columnNames'] = [str(x) for x in columns] + # You cannot have rows without columns + rows = table.get('rows', None) + if rows: + tmp['rows'] = [] + for row in rows: + tmp['rows'].append(dict(row=[str(x) for x in row['row']])) + result.append(tmp) + result = sorted(result, key=lambda k: k['name']) + return result + + @tables.setter + def tables(self, value): + self._values['tables'] = value + + @property + def variables(self): + result = [] + if not self._values['variables']: + return None + variables = self._values['variables'] + for variable in variables: + tmp = dict((str(k), str(v)) for k, v in iteritems(variable)) + if 'encrypted' not in tmp: + # BIG-IP will inject an 'encrypted' key if you don't provide one. + # If you don't provide one, then we give you the default 'no', by + # default. + tmp['encrypted'] = 'no' + if 'value' not in tmp: + tmp['value'] = '' + + # This seems to happen only on 12.0.0 + elif tmp['value'] == 'none': + tmp['value'] = '' + result.append(tmp) + result = sorted(result, key=lambda k: k['name']) + return result + + @variables.setter + def variables(self, value): + self._values['variables'] = value + + @property + def lists(self): + result = [] + if not self._values['lists']: + return None + lists = self._values['lists'] + for list in lists: + tmp = dict((str(k), str(v)) for k, v in iteritems(list) if k != 'value') + if 'encrypted' not in list: + # BIG-IP will inject an 'encrypted' key if you don't provide one. + # If you don't provide one, then we give you the default 'no', by + # default. + tmp['encrypted'] = 'no' + if 'value' in list: + if len(list['value']) > 0: + # BIG-IP removes empty values entries, so mimic this behavior + # for user-supplied values. + tmp['value'] = [str(x) for x in list['value']] + result.append(tmp) + result = sorted(result, key=lambda k: k['name']) + return result + + @lists.setter + def lists(self, value): + self._values['lists'] = value + + @property + def parameters(self): + return dict( + tables=self.tables, + variables=self.variables, + lists=self.lists + ) + + @parameters.setter + def parameters(self, value): + if value is None: + return + if 'tables' in value: + self.tables = value['tables'] + if 'variables' in value: + self.variables = value['variables'] + if 'lists' in value: + self.lists = value['lists'] + + @property + def template(self): + if self._values['template'] is None: + return None + if self._values['template'].startswith("/" + self.partition): + return self._values['template'] + else: + return '/{0}/{1}'.format( + self.partition, self._values['template'] + ) + + @template.setter + def template(self, value): + self._values['template'] = value + + +class ModuleManager(object): + def __init__(self, client): + self.client = client + self.have = None + 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 _update_changed_options(self): + changed = {} + for key in Parameters.updatables: + if getattr(self.want, key) is not None: + attr1 = getattr(self.want, key) + attr2 = getattr(self.have, key) + if attr1 != attr2: + changed[key] = str(DeepDiff(attr1, attr2)) + if changed: + self.changes = Parameters(changed) + return True + return False + + def exec_module(self): + changed = False + result = dict() + state = self.want.state + + try: + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + changes = self.changes.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + return result + + def exists(self): + result = self.client.api.tm.sys.application.services.service.exists( + name=self.want.name, + partition=self.want.partition + ) + return result + + def present(self): + if self.exists(): + return self.update() + else: + return self.create() + + def create(self): + self._set_changed_options() + if self.client.check_mode: + return True + self.create_on_device() + return True + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update() and not self.want.force: + return False + if self.client.check_mode: + return True + self.update_on_device() + return True + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update_on_device(self): + params = self.want.api_params() + params['execute-action'] = 'definition' + resource = self.client.api.tm.sys.application.services.service.load( + name=self.want.name, + partition=self.want.partition + ) + resource.update(**params) + + def read_current_from_device(self): + result = self.client.api.tm.sys.application.services.service.load( + name=self.want.name, + partition=self.want.partition + ).to_dict() + result.pop('_meta_data', None) + return Parameters(result) + + def create_on_device(self): + params = self.want.api_params() + self.client.api.tm.sys.application.services.service.create( + name=self.want.name, + partition=self.want.partition, + **params + ) + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.client.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the iApp service") + return True + + def remove_from_device(self): + resource = self.client.api.tm.sys.application.services.service.load( + name=self.want.name, + partition=self.want.partition + ) + if resource: + resource.delete() + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.argument_spec = dict( + name=dict(required=True), + template=dict(), + parameters=dict( + type='dict' + ), + state=dict( + default='present', + choices=['absent', 'present'] + ), + force=dict( + default=False, + type='bool' + ) + ) + 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/fixtures/create_iapp_service_parameters_f5_http.json b/test/units/modules/network/f5/fixtures/create_iapp_service_parameters_f5_http.json new file mode 100644 index 0000000000..86a95ecf85 --- /dev/null +++ b/test/units/modules/network/f5/fixtures/create_iapp_service_parameters_f5_http.json @@ -0,0 +1,195 @@ +{ + "name": "http_example", + "partition": "Common", + "template": "/Common/f5.http", + "lists": [ + { + "name": "irules__irules", + "encrypted": "no", + "value": [ + "/Common/lgyft" + ] + }, + { + "name": "net__client_vlan", + "encrypted": "no", + "value": [ + "/Common/net2" + ] + } + ], + "tables": [ + { + "columnNames": [ + "name" + ], + "name": "pool__hosts", + "rows": [ + { + "row": [ + "demo.example.com" + ] + } + ] + }, + { + "columnNames": [ + "addr", + "connection_limit" + ], + "name": "pool__members", + "rows": [ + { + "row": [ + "10.1.1.1", + "0" + ] + }, + { + "row": [ + "10.1.1.2", + "0" + ] + } + ] + } + ], + "variables": [ + { + "name": "afm__policy", + "value": "/#do_not_use#" + }, + { + "name": "afm__dos_security_profile", + "value": "/#do_not_use#" + }, + { + "name": "afm__protocol_security_profile", + "value": "/#do_not_use#" + }, + { + "name": "asm__use_asm", + "value": "/#do_not_use#" + }, + { + "name": "client__http_compression", + "value": "/#do_not_use#" + }, + { + "name": "client__standard_caching_without_wa", + "value": "/#do_not_use#" + }, + { + "name": "client__tcp_wan_opt", + "value": "/#create_new#" + }, + { + "name": "monitor__monitor", + "value": "/#create_new#" + }, + { + "name": "monitor__frequency", + "value": "30" + }, + { + "name": "monitor__uri", + "value": "/my/path" + }, + { + "name": "monitor__response", + "value": "" + }, + { + "name": "net__client_mode", + "value": "wan" + }, + { + "name": "net__server_mode", + "value": "lan" + }, + { + "name": "net__vlan_mode", + "value": "all" + }, + { + "name": "pool__addr", + "value": "10.10.10.10" + }, + { + "name": "pool__http", + "value": "/#create_new#" + }, + { + "name": "pool__mask", + "value": "" + }, + { + "name": "pool__persist", + "value": "/#cookie#" + }, + { + "name": "pool__lb_method", + "value": "least-connections-member" + }, + { + "name": "pool__pool_to_use", + "value": "/#create_new#" + }, + { + "name": "pool__port_secure", + "value": "443" + }, + { + "name": "pool__redirect_port", + "value": "80" + }, + { + "name": "pool__redirect_to_https", + "value": "yes" + }, + { + "name": "pool__xff", + "value": "yes" + }, + { + "name": "server__oneconnect", + "value": "/#create_new#" + }, + { + "name": "server__tcp_lan_opt", + "value": "/#create_new#" + }, + { + "name": "ssl__cert", + "value": "/Common/default.crt" + }, + { + "name": "ssl__client_ssl_profile", + "value": "/#create_new#" + }, + { + "name": "ssl__key", + "value": "/Common/default.key" + }, + { + "name": "ssl__mode", + "value": "client_ssl" + }, + { + "name": "ssl__use_chain_cert", + "value": "/#do_not_use#" + }, + { + "name": "ssl_encryption_questions__advanced", + "value": "yes" + }, + { + "name": "stats__analytics", + "value": "/#do_not_use#" + }, + { + "name": "stats__request_logging", + "value": "/#do_not_use#" + } + ] +} diff --git a/test/units/modules/network/f5/fixtures/update_iapp_service_parameters_f5_http.json b/test/units/modules/network/f5/fixtures/update_iapp_service_parameters_f5_http.json new file mode 100644 index 0000000000..cbb389364d --- /dev/null +++ b/test/units/modules/network/f5/fixtures/update_iapp_service_parameters_f5_http.json @@ -0,0 +1,195 @@ +{ + "name": "http_example", + "partition": "Common", + "template": "/Common/f5.http", + "lists": [ + { + "name": "irules__irules", + "encrypted": "no", + "value": [ + "/Common/lgyft" + ] + }, + { + "name": "net__client_vlan", + "encrypted": "no", + "value": [ + "/Common/net2" + ] + } + ], + "tables": [ + { + "columnNames": [ + "name" + ], + "name": "pool__hosts", + "rows": [ + { + "row": [ + "demo.example.com" + ] + } + ] + }, + { + "columnNames": [ + "addr", + "connection_limit" + ], + "name": "pool__members", + "rows": [ + { + "row": [ + "20.1.1.1", + "0" + ] + }, + { + "row": [ + "10.1.1.2", + "0" + ] + } + ] + } + ], + "variables": [ + { + "name": "afm__policy", + "value": "/#do_not_use#" + }, + { + "name": "afm__dos_security_profile", + "value": "/#do_not_use#" + }, + { + "name": "afm__protocol_security_profile", + "value": "/#do_not_use#" + }, + { + "name": "asm__use_asm", + "value": "/#do_not_use#" + }, + { + "name": "client__http_compression", + "value": "/#do_not_use#" + }, + { + "name": "client__standard_caching_without_wa", + "value": "/#do_not_use#" + }, + { + "name": "client__tcp_wan_opt", + "value": "/#create_new#" + }, + { + "name": "monitor__monitor", + "value": "/#create_new#" + }, + { + "name": "monitor__frequency", + "value": "30" + }, + { + "name": "monitor__uri", + "value": "/my/path" + }, + { + "name": "monitor__response", + "value": "" + }, + { + "name": "net__client_mode", + "value": "wan" + }, + { + "name": "net__server_mode", + "value": "lan" + }, + { + "name": "net__vlan_mode", + "value": "all" + }, + { + "name": "pool__addr", + "value": "10.10.10.10" + }, + { + "name": "pool__http", + "value": "/#create_new#" + }, + { + "name": "pool__mask", + "value": "" + }, + { + "name": "pool__persist", + "value": "/#cookie#" + }, + { + "name": "pool__lb_method", + "value": "least-connections-member" + }, + { + "name": "pool__pool_to_use", + "value": "/#create_new#" + }, + { + "name": "pool__port_secure", + "value": "443" + }, + { + "name": "pool__redirect_port", + "value": "80" + }, + { + "name": "pool__redirect_to_https", + "value": "yes" + }, + { + "name": "pool__xff", + "value": "yes" + }, + { + "name": "server__oneconnect", + "value": "/#create_new#" + }, + { + "name": "server__tcp_lan_opt", + "value": "/#create_new#" + }, + { + "name": "ssl__cert", + "value": "/Common/default.crt" + }, + { + "name": "ssl__client_ssl_profile", + "value": "/#create_new#" + }, + { + "name": "ssl__key", + "value": "/Common/default.key" + }, + { + "name": "ssl__mode", + "value": "client_ssl" + }, + { + "name": "ssl__use_chain_cert", + "value": "/#do_not_use#" + }, + { + "name": "ssl_encryption_questions__advanced", + "value": "yes" + }, + { + "name": "stats__analytics", + "value": "/#do_not_use#" + }, + { + "name": "stats__request_logging", + "value": "/#do_not_use#" + } + ] +} diff --git a/test/units/modules/network/f5/test_bigip_iapp_service.py b/test/units/modules/network/f5/test_bigip_iapp_service.py new file mode 100644 index 0000000000..2b4358731e --- /dev/null +++ b/test/units/modules/network/f5/test_bigip_iapp_service.py @@ -0,0 +1,264 @@ +# -*- 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_iapp_service import Parameters + from library.bigip_iapp_service import ModuleManager + from library.bigip_iapp_service import ArgumentSpec +except ImportError: + try: + from ansible.modules.network.f5.bigip_iapp_service import Parameters + from ansible.modules.network.f5.bigip_iapp_service import ModuleManager + from ansible.modules.network.f5.bigip_iapp_service 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_keys(self): + args = load_fixture('create_iapp_service_parameters_f5_http.json') + p = Parameters(args) + + # Assert the top-level keys + assert p.name == 'http_example' + assert p.partition == 'Common' + assert p.template == '/Common/f5.http' + + def test_module_parameters_lists(self): + args = load_fixture('create_iapp_service_parameters_f5_http.json') + p = Parameters(args) + + assert 'lists' in p._values + + assert p.lists[0]['name'] == 'irules__irules' + assert p.lists[0]['encrypted'] == 'no' + assert len(p.lists[0]['value']) == 1 + assert p.lists[0]['value'][0] == '/Common/lgyft' + + assert p.lists[1]['name'] == 'net__client_vlan' + assert p.lists[1]['encrypted'] == 'no' + assert len(p.lists[1]['value']) == 1 + assert p.lists[1]['value'][0] == '/Common/net2' + + def test_module_parameters_tables(self): + args = load_fixture('create_iapp_service_parameters_f5_http.json') + p = Parameters(args) + + assert 'tables' in p._values + + assert 'columnNames' in p.tables[0] + assert len(p.tables[0]['columnNames']) == 1 + assert p.tables[0]['columnNames'][0] == 'name' + + assert 'name' in p.tables[0] + assert p.tables[0]['name'] == 'pool__hosts' + + assert 'rows' in p.tables[0] + assert len(p.tables[0]['rows']) == 1 + assert 'row' in p.tables[0]['rows'][0] + assert len(p.tables[0]['rows'][0]['row']) == 1 + assert p.tables[0]['rows'][0]['row'][0] == 'demo.example.com' + + assert len(p.tables[1]['rows']) == 2 + assert 'row' in p.tables[0]['rows'][0] + assert len(p.tables[1]['rows'][0]['row']) == 2 + assert p.tables[1]['rows'][0]['row'][0] == '10.1.1.1' + assert p.tables[1]['rows'][0]['row'][1] == '0' + assert p.tables[1]['rows'][1]['row'][0] == '10.1.1.2' + assert p.tables[1]['rows'][1]['row'][1] == '0' + + def test_module_parameters_variables(self): + args = load_fixture('create_iapp_service_parameters_f5_http.json') + p = Parameters(args) + + assert 'variables' in p._values + assert len(p.variables) == 34 + + # Assert one configuration value + assert 'name' in p.variables[0] + assert 'value' in p.variables[0] + assert p.variables[0]['name'] == 'afm__dos_security_profile' + assert p.variables[0]['value'] == '/#do_not_use#' + + # Assert a second configuration value + assert 'name' in p.variables[1] + assert 'value' in p.variables[1] + assert p.variables[1]['name'] == 'afm__policy' + assert p.variables[1]['value'] == '/#do_not_use#' + + def test_api_parameters_variables(self): + args = dict( + variables=[ + dict( + name="client__http_compression", + encrypted="no", + value="/#create_new#" + ) + ] + ) + p = Parameters(args) + assert p.variables[0]['name'] == 'client__http_compression' + + def test_api_parameters_tables(self): + args = dict( + tables=[ + { + "name": "pool__members", + "columnNames": [ + "addr", + "port", + "connection_limit" + ], + "rows": [ + { + "row": [ + "12.12.12.12", + "80", + "0" + ] + }, + { + "row": [ + "13.13.13.13", + "443", + 10 + ] + } + ] + } + ] + ) + p = Parameters(args) + assert p.tables[0]['name'] == 'pool__members' + assert p.tables[0]['columnNames'] == ['addr', 'port', 'connection_limit'] + assert len(p.tables[0]['rows']) == 2 + assert 'row' in p.tables[0]['rows'][0] + assert 'row' in p.tables[0]['rows'][1] + assert p.tables[0]['rows'][0]['row'] == ['12.12.12.12', '80', '0'] + assert p.tables[0]['rows'][1]['row'] == ['13.13.13.13', '443', '10'] + + +@patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root', + return_value=True) +class TestManager(unittest.TestCase): + + def setUp(self): + self.spec = ArgumentSpec() + + def test_create_service(self, *args): + parameters = load_fixture('create_iapp_service_parameters_f5_http.json') + set_module_args(dict( + name='foo', + template='f5.http', + parameters=parameters, + state='present', + password='passsword', + server='localhost', + user='admin' + )) + + client = AnsibleF5Client( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode, + f5_product_name=self.spec.f5_product_name + ) + mm = ModuleManager(client) + + # Override methods to force specific logic in the module to happen + mm.exists = Mock(return_value=False) + mm.create_on_device = Mock(return_value=True) + + results = mm.exec_module() + assert results['changed'] is True + + def test_update_agent_status_traps(self, *args): + parameters = load_fixture('update_iapp_service_parameters_f5_http.json') + set_module_args(dict( + name='foo', + template='f5.http', + parameters=parameters, + state='present', + password='passsword', + server='localhost', + user='admin' + )) + + # Configure the parameters that would be returned by querying the + # remote device + parameters = load_fixture('create_iapp_service_parameters_f5_http.json') + current = Parameters(parameters) + + client = AnsibleF5Client( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode, + f5_product_name=self.spec.f5_product_name + ) + mm = ModuleManager(client) + + # Override methods to force specific logic in the module to happen + mm.exists = Mock(return_value=True) + mm.update_on_device = Mock(return_value=True) + mm.read_current_from_device = Mock(return_value=current) + + results = mm.exec_module() + assert results['changed'] is True