diff --git a/lib/ansible/modules/network/f5/bigip_iapp_service.py b/lib/ansible/modules/network/f5/bigip_iapp_service.py index eead532ae58..45e627445e5 100644 --- a/lib/ansible/modules/network/f5/bigip_iapp_service.py +++ b/lib/ansible/modules/network/f5/bigip_iapp_service.py @@ -19,9 +19,7 @@ short_description: Manages TCL iApp services on a BIG-IP description: - Manages TCL iApp services on a BIG-IP. - If you are looking for the API that is communicated with on the BIG-IP, - the one the is used is C(/mgmt/tm/sys/application/service/). There are a - couple of APIs in a BIG-IP that might seem like they are relevant to iApp - Services, but the API mentioned here is the one that is used. + the one the is used is C(/mgmt/tm/sys/application/service/). version_added: 2.4 options: name: @@ -32,13 +30,13 @@ options: 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). + create a service. + - When creating a new service, this parameter is required. 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 + it is recommended you use either the C(file) or C(template) lookups to supply the expected parameters. - These parameters typically consist of the C(lists), C(tables), and C(variables) fields. @@ -74,7 +72,7 @@ options: 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 + over any similar setting in the iApp Service payload that you provide in the C(parameters) field. default: yes type: bool @@ -85,9 +83,30 @@ options: 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 + over any similar setting in the iApp Service payload that you provide in the C(parameters) field. version_added: 2.5 + metadata: + description: + - Metadata associated with the iApp service. + - If this option is specified in the Ansible task, it will take precedence + over any similar setting in the iApp Service payload that you provide in + the C(parameters) field. + version_added: 2.7 + description: + description: + - Description of the iApp service. + - If this option is specified in the Ansible task, it will take precedence + over any similar setting in the iApp Service payload that you provide in + the C(parameters) field. + version_added: 2.7 + device_group: + description: + - The device group for the iApp service. + - If this option is specified in the Ansible task, it will take precedence + over any similar setting in the iApp Service payload that you provide in + the C(parameters) field. + version_added: 2.7 extends_documentation_fragment: f5 author: - Tim Rupp (@caphrim007) @@ -210,6 +229,22 @@ EXAMPLES = r''' - 0 - name: server_pools__servers delegate_to: localhost + +- name: Override metadata that may or may not exist in parameters + bigip_iapp_service: + name: foo-service + template: f5.http + parameters: "{{ lookup('file', 'f5.http.parameters.json') }}" + metadata: + - persist: yes + name: data 1 + - persist: yes + name: data 2 + password: secret + server: lb.mydomain.com + state: present + user: admin + delegate_to: localhost ''' RETURN = r''' @@ -228,6 +263,7 @@ try: 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 + from library.module_utils.network.f5.common import flatten_boolean try: from library.module_utils.network.f5.common import iControlUnexpectedHTTPError except ImportError: @@ -240,6 +276,7 @@ except ImportError: 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 + from ansible.module_utils.network.f5.common import flatten_boolean try: from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError except ImportError: @@ -250,17 +287,45 @@ class Parameters(AnsibleF5Parameters): api_map = { 'strictUpdates': 'strict_updates', 'trafficGroup': 'traffic_group', + 'deviceGroup': 'device_group', } - returnables = [] - - api_attributes = [ - 'tables', 'variables', 'template', 'lists', 'deviceGroup', - 'inheritedDevicegroup', 'inheritedTrafficGroup', 'trafficGroup', - 'strictUpdates' + returnables = [ + 'tables', + 'variables', + 'lists', + 'strict_updates', + 'traffic_group', + 'device_group', + 'metadata', + 'template', + 'description', ] - updatables = ['tables', 'variables', 'lists', 'strict_updates', 'traffic_group'] + api_attributes = [ + 'tables', + 'variables', + 'template', + 'lists', + 'deviceGroup', + 'inheritedDevicegroup', + 'inheritedTrafficGroup', + 'trafficGroup', + 'strictUpdates', + # 'metadata', + 'description', + ] + + updatables = [ + 'tables', + 'variables', + 'lists', + 'strict_updates', + 'device_group', + 'traffic_group', + 'metadata', + 'description', + ] def to_return(self): result = {} @@ -269,12 +334,8 @@ class Parameters(AnsibleF5Parameters): result = self._filter_params(result) return result - @property - def tables(self): + def normalize_tables(self, tables): result = [] - if not self._values['tables']: - return None - tables = self._values['tables'] for table in tables: tmp = dict() name = table.get('name', None) @@ -296,16 +357,8 @@ class Parameters(AnsibleF5Parameters): result = sorted(result, key=lambda k: k['name']) return result - @tables.setter - def tables(self, value): - self._values['tables'] = value - - @property - def variables(self): + def normalize_variables(self, variables): 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: @@ -323,16 +376,8 @@ class Parameters(AnsibleF5Parameters): result = sorted(result, key=lambda k: k['name']) return result - @variables.setter - def variables(self, value): - self._values['variables'] = value - - @property - def lists(self): + def normalize_list(self, lists): 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: @@ -349,39 +394,148 @@ class Parameters(AnsibleF5Parameters): result = sorted(result, key=lambda k: k['name']) return result - @lists.setter - def lists(self, value): - self._values['lists'] = value - - @property - def parameters(self): - result = dict( - tables=self.tables, - variables=self.variables, - lists=self.lists - ) + def normalize_metadata(self, metadata): + result = [] + for item in metadata: + name = item.get('name', None) + persist = flatten_boolean(item.get('persist', "no")) + if persist == "yes": + persist = "true" + else: + persist = "false" + result.append({ + "name": name, + "persist": persist + }) return result - @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'] - if 'deviceGroup' in value: - self.deviceGroup = value['deviceGroup'] - if 'inheritedDevicegroup' in value: - self.inheritedDevicegroup = value['inheritedDevicegroup'] - if 'inheritedTrafficGroup' in value: - self.inheritedTrafficGroup = value['inheritedTrafficGroup'] - if 'trafficGroup' in value: - self.trafficGroup = value['trafficGroup'] - if 'strictUpdates' in value: - self.strictUpdates = value['strictUpdates'] + +class ApiParameters(Parameters): + @property + def metadata(self): + if self._values['metadata'] is None: + return None + return self._values['metadata'] + + @property + def tables(self): + if self._values['tables'] is None: + return None + return self.normalize_tables(self._values['tables']) + + @property + def lists(self): + if self._values['lists'] is None: + return None + return self.normalize_list(self._values['lists']) + + @property + def variables(self): + if self._values['variables'] is None: + return None + return self.normalize_variables(self._values['variables']) + + @property + def device_group(self): + if self._values['device_group'] in [None, 'none']: + return None + return self._values['device_group'] + + +class ModuleParameters(Parameters): + @property + def param_lists(self): + if self._values['parameters'] is None: + return None + result = self._values['parameters'].get('lists', None) + return result + + @property + def param_tables(self): + if self._values['parameters'] is None: + return None + result = self._values['parameters'].get('tables', None) + return result + + @property + def param_variables(self): + if self._values['parameters'] is None: + return None + result = self._values['parameters'].get('variables', None) + return result + + @property + def param_metadata(self): + if self._values['parameters'] is None: + return None + result = self._values['parameters'].get('metadata', None) + return result + + @property + def param_description(self): + if self._values['parameters'] is None: + return None + result = self._values['parameters'].get('description', None) + return result + + @property + def param_traffic_group(self): + if self._values['parameters'] is None: + return None + result = self._values['parameters'].get('trafficGroup', None) + if not result: + return result + return fq_name(self.partition, result) + + @property + def param_device_group(self): + if self._values['parameters'] is None: + return None + result = self._values['parameters'].get('deviceGroup', None) + if not result: + return result + return fq_name(self.partition, result) + + @property + def param_strict_updates(self): + if self._values['parameters'] is None: + return None + result = self._values['parameters'].get('strictUpdates', None) + return flatten_boolean(result) + + @property + def tables(self): + if self._values['tables']: + return self.normalize_tables(self._values['tables']) + elif self.param_tables: + return self.normalize_tables(self.param_tables) + return None + + @property + def lists(self): + if self._values['lists']: + return self.normalize_list(self._values['lists']) + elif self.param_lists: + return self.normalize_list(self.param_lists) + return None + + @property + def variables(self): + if self._values['variables']: + return self.normalize_variables(self._values['variables']) + elif self.param_variables: + return self.normalize_variables(self.param_variables) + return None + + @property + def metadata(self): + if self._values['metadata']: + result = self.normalize_metadata(self._values['metadata']) + elif self.param_metadata: + result = self.normalize_metadata(self.param_metadata) + else: + return None + return result @property def template(self): @@ -389,55 +543,67 @@ class Parameters(AnsibleF5Parameters): return None return fq_name(self.partition, 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 + def device_group(self): + if self._values['device_group'] not in [None, 'none']: + result = fq_name(self.partition, self._values['device_group']) + elif self.param_device_group not in [None, 'none']: + result = self.param_device_group else: - return self._values['strict_updates'] + return None + if not result.startswith('/Common/'): + raise F5ModuleError( + "Device groups can only exist in /Common" + ) + return result @property def traffic_group(self): - if self._values['traffic_group'] is None and self.trafficGroup is None: + if self._values['traffic_group']: + result = fq_name(self.partition, self._values['traffic_group']) + elif self.param_traffic_group: + result = self.param_traffic_group + else: return None - - # Specifying the value overrides any associated value in the payload - elif self._values['traffic_group']: - result = fq_name(self.partition, self._values['traffic_group']) - - # This will be automatically `None` if it was not set by the - # `parameters` setter - elif self.trafficGroup: - result = fq_name(self.partition, self.trafficGroup) - else: - result = fq_name(self.partition, self._values['traffic_group']) - if result.startswith('/Common/'): - return result - else: + if not result.startswith('/Common/'): raise F5ModuleError( "Traffic groups can only exist in /Common" ) + return result + + @property + def strict_updates(self): + if self._values['strict_updates'] is not None: + result = flatten_boolean(self._values['strict_updates']) + elif self.param_strict_updates is not None: + result = self.param_strict_updates + else: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def description(self): + if self._values['description']: + return self._values['description'] + elif self.param_description: + return self.param_description + return None class Changes(Parameters): pass +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + class Difference(object): def __init__(self, want, have=None): self.want = want @@ -460,9 +626,17 @@ class Difference(object): return attr1 @property - def traffic_group(self): - if self.want.traffic_group != self.have.traffic_group: - return self.want.traffic_group + def metadata(self): + if self.want.metadata is None: + return None + if self.have.metadata is None: + return self.want.metadata + want = [(k, v) for d in self.want.metadata for k, v in iteritems(d)] + have = [(k, v) for d in self.have.metadata for k, v in iteritems(d)] + if set(want) != set(have): + return dict( + metadata=self.want.metadata + ) class ModuleManager(object): @@ -470,8 +644,8 @@ class ModuleManager(object): self.module = kwargs.get('module', None) self.client = kwargs.get('client', None) self.have = None - self.want = Parameters(params=self.module.params) - self.changes = Changes() + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() def _set_changed_options(self): changed = {} @@ -479,7 +653,7 @@ class ModuleManager(object): if getattr(self.want, key) is not None: changed[key] = getattr(self.want, key) if changed: - self.changes = Changes(params=changed) + self.changes = UsableChanges(params=changed) def _update_changed_options(self): diff = Difference(self.want, self.have) @@ -495,7 +669,7 @@ class ModuleManager(object): else: changed[k] = change if changed: - self.changes = Changes(params=changed) + self.changes = UsableChanges(params=changed) return True return False @@ -532,8 +706,12 @@ class ModuleManager(object): def create(self): self._set_changed_options() - if self.want.traffic_group is None and self.want.trafficGroup is None: + if self.want.traffic_group is None: self.want.update({'traffic_group': '/Common/traffic-group-1'}) + if not self.template_exists(): + raise F5ModuleError( + "The specified template does not exist in the provided partition." + ) if self.module.check_mode: return True self.create_on_device() @@ -555,13 +733,20 @@ class ModuleManager(object): return False def update_on_device(self): - params = self.want.api_params() - params['execute-action'] = 'definition' + params = self.changes.api_params() resource = self.client.api.tm.sys.application.services.service.load( name=self.want.name, partition=self.want.partition ) - resource.update(**params) + if params: + params['execute-action'] = 'definition' + resource.update(**params) + if self.changes.metadata: + params = {'execute-action': 'definition'} + resource.update( + metadata=self.changes.metadata, + **params + ) def read_current_from_device(self): result = self.client.api.tm.sys.application.services.service.load( @@ -569,15 +754,26 @@ class ModuleManager(object): partition=self.want.partition ).to_dict() result.pop('_meta_data', None) - return Parameters(params=result) + return ApiParameters(params=result) + + def template_exists(self): + name = fq_name(self.want.partition, self.want.template) + parts = name.split('/') + result = self.client.api.tm.sys.application.templates.template.exists( + name=parts[2], + partition=parts[1] + ) + return result def create_on_device(self): - params = self.want.api_params() - self.client.api.tm.sys.application.services.service.create( + params = self.changes.api_params() + resource = self.client.api.tm.sys.application.services.service.create( name=self.want.name, partition=self.want.partition, **params ) + if self.changes.metadata: + resource.update(metadata=self.changes.metadata) def absent(self): if self.exists(): @@ -598,6 +794,13 @@ class ModuleManager(object): partition=self.want.partition ) if resource: + # Metadata needs to be zero'd before the service is removed because + # otherwise, the API will error out saying that "configuration items" + # currently exist. + # + # In other words, the REST API is not able to delete a service while + # there is existing metadata + resource.update(metadata=[]) resource.delete() @@ -607,6 +810,8 @@ class ArgumentSpec(object): argument_spec = dict( name=dict(required=True), template=dict(), + description=dict(), + device_group=dict(), parameters=dict( type='dict' ), @@ -622,6 +827,7 @@ class ArgumentSpec(object): type='bool', default='yes' ), + metadata=dict(type='list'), traffic_group=dict(), partition=dict( default='Common',