From 015baf51494f7d26db2ecf3ef4f8aab9d16a105c Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 1 Nov 2017 21:47:56 -0700 Subject: [PATCH] Removes bigip_iapp_service from skip list (#32482) --- .../modules/network/f5/bigip_iapp_service.py | 377 ++++++++++++++---- test/sanity/import/skip.txt | 1 - .../network/f5/test_bigip_iapp_service.py | 88 +++- 3 files changed, 361 insertions(+), 105 deletions(-) diff --git a/lib/ansible/modules/network/f5/bigip_iapp_service.py b/lib/ansible/modules/network/f5/bigip_iapp_service.py index 625d2e0d28..d9d0e074aa 100644 --- a/lib/ansible/modules/network/f5/bigip_iapp_service.py +++ b/lib/ansible/modules/network/f5/bigip_iapp_service.py @@ -4,14 +4,18 @@ # 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': 'community'} -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: bigip_iapp_service -short_description: Manages TCL iApp services on a BIG-IP. +short_description: Manages TCL iApp services on a BIG-IP description: - Manages TCL iApp services on a BIG-IP. version_added: "2.4" @@ -32,6 +36,8 @@ options: 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. + - These parameters typically consist of the C(lists), C(tables), and + C(variables) fields. force: description: - Forces the updating of an iApp service even if the parameters to the @@ -39,7 +45,7 @@ options: 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 + default: no state: description: - When C(present), ensures that the iApp service is created and running. @@ -48,105 +54,226 @@ options: choices: - present - absent + partition: + description: + - Device partition to manage resources on. + default: Common + version_added: 2.5 + strict_updates: + description: + - Indicates whether the application service is tied to the template, + so when the template is updated, the application service changes to + reflect the updates. + - When C(yes), disallows any updates to the resources that the iApp + service has created, if they are not updated directly through the + iApp. + - When C(no), allows updates outside of the iApp. + - If this option is specified in the Ansible task, it will take precedence + over any similar setting in the iApp Server payload that you provide in + the C(parameters) field. + default: yes + version_added: 2.5 + traffic_group: + description: + - The traffic group for the iApp service. When creating a new service, if + this value is not specified, the default of C(/Common/traffic-group-1) + will be used. + - If this option is specified in the Ansible task, it will take precedence + over any similar setting in the iApp Server payload that you provide in + the C(parameters) field. + version_added: 2.5 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 = ''' +EXAMPLES = r''' - 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" + 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" + 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" + 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" + 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 + +- name: Try to remove the iApp template before the associated Service is removed + bigip_iapp_template: + name: web_frontends + state: absent + register: result + failed_when: + - not result|success + - "'referenced by one or more applications' not in result.msg" + +- name: Configure a service using more complicated parameters + 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 + lists: + - name: irules__irules + value: + - foo + - bar + tables: + - name: basic__snatpool_members + - name: net__snatpool_members + - name: optimizations__hosts + - name: pool__hosts + columnNames: + - name + rows: + - row: + - internal.company.bar + - name: pool__members + columnNames: + - addr + - port + - connection_limit + rows: + - row: + - "none" + - 80 + - 0 + - name: server_pools__servers delegate_to: localhost ''' -RETURN = ''' +RETURN = r''' # only common fields returned ''' -from ansible.module_utils.f5_utils import ( - AnsibleF5Client, - AnsibleF5Parameters, - HAS_F5SDK, - F5ModuleError, - iteritems, - iControlUnexpectedHTTPError -) -from deepdiff import DeepDiff +from ansible.module_utils.f5_utils import AnsibleF5Client +from ansible.module_utils.f5_utils import AnsibleF5Parameters +from ansible.module_utils.f5_utils import HAS_F5SDK +from ansible.module_utils.f5_utils import F5ModuleError +from ansible.module_utils.six import iteritems +from collections import defaultdict + +try: + from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError +except ImportError: + HAS_F5SDK = False class Parameters(AnsibleF5Parameters): + api_map = { + 'strictUpdates': 'strict_updates', + 'trafficGroup': 'traffic_group', + } returnables = [] api_attributes = [ 'tables', 'variables', 'template', 'lists', 'deviceGroup', - 'inheritedDevicegroup', 'inheritedTrafficGroup', 'trafficGroup' + 'inheritedDevicegroup', 'inheritedTrafficGroup', 'trafficGroup', + 'strictUpdates' ] - updatables = ['tables', 'variables', 'lists'] + updatables = ['tables', 'variables', 'lists', 'strict_updates', 'traffic_group'] + + def __init__(self, params=None): + self._values = defaultdict(lambda: None) + if params: + self.update(params=params) + self._values['__warnings'] = [] + + def update(self, params=None): + if params: + for k, v in iteritems(params): + if self.api_map is not None and k in self.api_map: + map_key = self.api_map[k] + else: + map_key = k + + # Handle weird API parameters like `dns.proxy.__iter__` by + # using a map provided by the module developer + class_attr = getattr(type(self), map_key, None) + if isinstance(class_attr, property): + # There is a mapped value for the api_map key + if class_attr.fset is None: + # If the mapped value does not have an associated setter + self._values[map_key] = v + else: + # The mapped value has a setter + setattr(self, map_key, v) + else: + # If the mapped value is not a @property + self._values[map_key] = v + + def _fqdn_name(self, value): + if value is not None and not value.startswith('/'): + return '/{0}/{1}'.format(self.partition, value) + return value def to_return(self): result = {} @@ -251,11 +378,12 @@ class Parameters(AnsibleF5Parameters): @property def parameters(self): - return dict( + result = dict( tables=self.tables, variables=self.variables, lists=self.lists ) + return result @parameters.setter def parameters(self, value): @@ -275,31 +403,97 @@ class Parameters(AnsibleF5Parameters): self.inheritedTrafficGroup = value['inheritedTrafficGroup'] if 'trafficGroup' in value: self.trafficGroup = value['trafficGroup'] + if 'strictUpdates' in value: + self.strictUpdates = value['strictUpdates'] @property def template(self): if self._values['template'] is None: return None - if self._values['template'].startswith("/" + self.partition): - return self._values['template'] - elif self._values['template'].startswith("/"): - return self._values['template'] - else: - return '/{0}/{1}'.format( - self.partition, self._values['template'] - ) + return self._fqdn_name(self._values['template']) @template.setter def template(self, value): self._values['template'] = value + @property + def strict_updates(self): + if self._values['strict_updates'] is None and self.strictUpdates is None: + return None + + # Specifying the value overrides any associated value in the payload + elif self._values['strict_updates'] is True: + return 'enabled' + elif self._values['strict_updates'] is False: + return 'disabled' + + # This will be automatically `None` if it was not set by the + # `parameters` setter + elif self.strictUpdates: + return self.strictUpdates + else: + return self._values['strict_updates'] + + @property + def traffic_group(self): + if self._values['traffic_group'] is None and self.trafficGroup is None: + return None + + # Specifying the value overrides any associated value in the payload + elif self._values['traffic_group']: + result = self._fqdn_name(self._values['traffic_group']) + + # This will be automatically `None` if it was not set by the + # `parameters` setter + elif self.trafficGroup: + result = self._fqdn_name(self.trafficGroup) + else: + result = self._fqdn_name(self._values['traffic_group']) + if result.startswith('/Common/'): + return result + else: + raise F5ModuleError( + "Traffic groups can only exist in /Common" + ) + + +class Changes(Parameters): + pass + + +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 + + @property + def traffic_group(self): + if self.want.traffic_group != self.have.traffic_group: + return self.want.traffic_group + class ModuleManager(object): def __init__(self, client): self.client = client self.have = None self.want = Parameters(self.client.module.params) - self.changes = Parameters() + self.changes = Changes() def _set_changed_options(self): changed = {} @@ -307,18 +501,23 @@ class ModuleManager(object): if getattr(self.want, key) is not None: changed[key] = getattr(self.want, key) if changed: - self.changes = Parameters(changed) + self.changes = Changes(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)) + diff = Difference(self.want, self.have) + updatables = Parameters.updatables + changed = dict() + for k in updatables: + change = diff.compare(k) + if change is None: + continue + else: + if isinstance(change, dict): + changed.update(change) + else: + changed[k] = change if changed: - self.changes = Parameters(changed) + self.changes = Changes(changed) return True return False @@ -355,6 +554,8 @@ class ModuleManager(object): def create(self): self._set_changed_options() + if self.want.traffic_group is None and self.want.trafficGroup is None: + self.want.update({'traffic_group': '/Common/traffic-group-1'}) if self.client.check_mode: return True self.create_on_device() @@ -436,9 +637,13 @@ class ArgumentSpec(object): choices=['absent', 'present'] ), force=dict( - default=False, + default='no', type='bool' - ) + ), + strict_updates=dict( + type='bool' + ), + traffic_group=dict() ) self.f5_product_name = 'bigip' diff --git a/test/sanity/import/skip.txt b/test/sanity/import/skip.txt index db5f30b14f..cc88841b98 100644 --- a/test/sanity/import/skip.txt +++ b/test/sanity/import/skip.txt @@ -14,7 +14,6 @@ lib/ansible/modules/cloud/webfaction/webfaction_mailbox.py lib/ansible/modules/cloud/webfaction/webfaction_site.py lib/ansible/modules/clustering/consul_acl.py lib/ansible/modules/network/cloudengine/ce_file_copy.py -lib/ansible/modules/network/f5/bigip_iapp_service.py lib/ansible/modules/network/f5/bigip_iapp_template.py lib/ansible/modules/network/f5/bigip_irule.py lib/ansible/modules/network/f5/bigip_pool.py diff --git a/test/units/modules/network/f5/test_bigip_iapp_service.py b/test/units/modules/network/f5/test_bigip_iapp_service.py index 64090c731a..8f7fd425c4 100644 --- a/test/units/modules/network/f5/test_bigip_iapp_service.py +++ b/test/units/modules/network/f5/test_bigip_iapp_service.py @@ -1,21 +1,7 @@ # -*- 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 . +# 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 @@ -38,11 +24,13 @@ try: from library.bigip_iapp_service import Parameters from library.bigip_iapp_service import ModuleManager from library.bigip_iapp_service import ArgumentSpec + from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError 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 + from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError except ImportError: raise SkipTest("F5 Ansible modules require the f5-sdk Python library") @@ -86,7 +74,7 @@ class TestParameters(unittest.TestCase): assert p.deviceGroup == 'none' assert p.inheritedTrafficGroup == 'true' assert p.inheritedDevicegroup == 'true' - assert p.trafficGroup == '/Common/traffic-group-local-only' + assert p.traffic_group == '/Common/traffic-group-local-only' def test_module_parameters_lists(self): args = load_fixture('create_iapp_service_parameters_f5_http.json') @@ -150,6 +138,70 @@ class TestParameters(unittest.TestCase): assert p.variables[1]['name'] == 'afm__policy' assert p.variables[1]['value'] == '/#do_not_use#' + def test_module_strict_updates_from_top_level(self): + # Assumes the user did not provide any parameters + + args = dict( + strict_updates=True + ) + p = Parameters(args) + assert p.strict_updates == 'enabled' + + args = dict( + strict_updates=False + ) + p = Parameters(args) + assert p.strict_updates == 'disabled' + + def test_module_strict_updates_override_from_top_level(self): + args = dict( + strict_updates=True, + parameters=dict( + strictUpdates='disabled' + ) + ) + p = Parameters(args) + assert p.strict_updates == 'enabled' + + args = dict( + strict_updates=False, + parameters=dict( + strictUpdates='enabled' + ) + ) + p = Parameters(args) + assert p.strict_updates == 'disabled' + + def test_module_strict_updates_only_parameters(self): + args = dict( + parameters=dict( + strictUpdates='disabled' + ) + ) + p = Parameters(args) + assert p.strict_updates == 'disabled' + + args = dict( + parameters=dict( + strictUpdates='enabled' + ) + ) + p = Parameters(args) + assert p.strict_updates == 'enabled' + + def test_api_strict_updates_from_top_level(self): + args = dict( + strictUpdates='enabled' + ) + p = Parameters(args) + assert p.strict_updates == 'enabled' + + args = dict( + strictUpdates='disabled' + ) + p = Parameters(args) + assert p.strict_updates == 'disabled' + def test_api_parameters_variables(self): args = dict( variables=[ @@ -227,7 +279,7 @@ class TestParameters(unittest.TestCase): trafficGroup='/Common/traffic-group-local-only' ) p = Parameters(args) - assert p.trafficGroup == '/Common/traffic-group-local-only' + assert p.traffic_group == '/Common/traffic-group-local-only' def test_module_template_same_partition(self): args = dict(