From 70e49b9243ffb3ce24d51676c348d8d7531b9610 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Tue, 9 Jan 2018 12:15:02 -0800 Subject: [PATCH] Updates module utils for f5 (#34660) These module utils are a refactor of the legacy ones and, in addition, there are several new methods and classes to support f5 modules going forward --- lib/ansible/module_utils/network/f5/bigip.py | 34 ++++ .../module_utils/network/f5/bigip/__init__.py | 0 .../module_utils/network/f5/bigip/common.py | 28 --- lib/ansible/module_utils/network/f5/bigiq.py | 31 ++++ lib/ansible/module_utils/network/f5/common.py | 166 +++++++++++++++++- .../module_utils/network/f5/iworkflow.py | 31 ++++ 6 files changed, 255 insertions(+), 35 deletions(-) create mode 100644 lib/ansible/module_utils/network/f5/bigip.py delete mode 100644 lib/ansible/module_utils/network/f5/bigip/__init__.py delete mode 100644 lib/ansible/module_utils/network/f5/bigip/common.py create mode 100644 lib/ansible/module_utils/network/f5/bigiq.py create mode 100644 lib/ansible/module_utils/network/f5/iworkflow.py diff --git a/lib/ansible/module_utils/network/f5/bigip.py b/lib/ansible/module_utils/network/f5/bigip.py new file mode 100644 index 0000000000..4db7c8576c --- /dev/null +++ b/lib/ansible/module_utils/network/f5/bigip.py @@ -0,0 +1,34 @@ +# -*- 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 + + +try: + from f5.bigip import ManagementRoot + from icontrol.exceptions import iControlUnexpectedHTTPError + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + +try: + from library.module_utils.network.f5.common import F5BaseClient +except ImportError: + from ansible.module_utils.network.f5.common import F5BaseClient + + +class F5Client(F5BaseClient): + @property + def api(self): + result = ManagementRoot( + self.params['server'], + self.params['user'], + self.params['password'], + port=self.params['server_port'], + verify=self.params['validate_certs'], + token='tmos' + ) + return result diff --git a/lib/ansible/module_utils/network/f5/bigip/__init__.py b/lib/ansible/module_utils/network/f5/bigip/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lib/ansible/module_utils/network/f5/bigip/common.py b/lib/ansible/module_utils/network/f5/bigip/common.py deleted file mode 100644 index 3babcee80b..0000000000 --- a/lib/ansible/module_utils/network/f5/bigip/common.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- 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 - - -try: - from f5.bigip import ManagementRoot as BigipManagementRoot - from f5.bigip.contexts import TransactionContextManager as BigipTransactionContextManager - from f5.bigiq import ManagementRoot as BigiqManagementRoot - from f5.iworkflow import ManagementRoot as IworkflowManagementRoot - from icontrol.exceptions import iControlUnexpectedHTTPError - HAS_F5SDK = True -except ImportError: - HAS_F5SDK = False - - -def cleanup_tokens(client): - try: - resource = client.api.shared.authz.tokens_s.token.load( - name=client.api.icrs.token - ) - resource.delete() - except Exception: - pass diff --git a/lib/ansible/module_utils/network/f5/bigiq.py b/lib/ansible/module_utils/network/f5/bigiq.py new file mode 100644 index 0000000000..5e23645835 --- /dev/null +++ b/lib/ansible/module_utils/network/f5/bigiq.py @@ -0,0 +1,31 @@ +# -*- 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 + + +try: + from f5.bigiq import ManagementRoot + from icontrol.exceptions import iControlUnexpectedHTTPError + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + +from ansible.module_utils.network.f5.common import F5BaseClient + + +class F5Client(F5BaseClient): + @property + def api(self): + result = ManagementRoot( + self.params['server'], + self.params['user'], + self.params['password'], + port=self.params['server_port'], + verify=self.params['validate_certs'], + token='local' + ) + return result diff --git a/lib/ansible/module_utils/network/f5/common.py b/lib/ansible/module_utils/network/f5/common.py index 8d16d32747..48ba4229cb 100644 --- a/lib/ansible/module_utils/network/f5/common.py +++ b/lib/ansible/module_utils/network/f5/common.py @@ -6,23 +6,61 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -from ansible.module_utils.network.common.utils import to_list, ComplexList -from ansible.module_utils.connection import exec_command from ansible.module_utils._text import to_text +from ansible.module_utils.basic import env_fallback +from ansible.module_utils.connection import exec_command +from ansible.module_utils.network.common.utils import to_list, ComplexList +from ansible.module_utils.six import iteritems +from collections import defaultdict + +try: + from icontrol.exceptions import iControlUnexpectedHTTPError + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + + +f5_provider_spec = { + 'server': dict(fallback=(env_fallback, ['F5_SERVER'])), + 'server_port': dict(type='int', default=443, fallback=(env_fallback, ['F5_SERVER_PORT'])), + 'user': dict(fallback=(env_fallback, ['F5_USER', 'ANSIBLE_NET_USERNAME'])), + 'password': dict(no_log=True, fallback=(env_fallback, ['F5_PASSWORD', 'ANSIBLE_NET_PASSWORD'])), + 'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + 'validate_certs': dict(type='bool', fallback=(env_fallback, ['F5_VALIDATE_CERTS'])), + 'transport': dict(default='rest', choices=['cli', 'rest']) +} + +f5_argument_spec = { + 'provider': dict(type='dict', options=f5_provider_spec), +} + +f5_top_spec = { + 'server': dict(removed_in_version=2.9, fallback=(env_fallback, ['F5_SERVER'])), + 'user': dict(removed_in_version=2.9, fallback=(env_fallback, ['F5_USER', 'ANSIBLE_NET_USERNAME'])), + 'password': dict(removed_in_version=2.9, no_log=True, fallback=(env_fallback, ['F5_PASSWORD'])), + 'validate_certs': dict(removed_in_version=2.9, type='bool', fallback=(env_fallback, ['F5_VALIDATE_CERTS'])), + 'server_port': dict(removed_in_version=2.9, type='int', default=443, fallback=(env_fallback, ['F5_SERVER_PORT'])), + 'transport': dict(removed_in_version=2.9, choices=['cli', 'rest']) +} +f5_argument_spec.update(f5_top_spec) + + +def get_provider_argspec(): + return f5_provider_spec # Fully Qualified name (with the partition) -def fq_name(partition, name): - if name is not None and not name.startswith('/'): - return '/%s/%s' % (partition, name) - return name +def fqdn_name(partition, value): + if value is not None and not value.startswith('/'): + return '/{0}/{1}'.format(partition, value) + return value # Fully Qualified name (with partition) for a list def fq_list_names(partition, list_names): if list_names is None: return None - return map(lambda x: fq_name(partition, x), list_names) + return map(lambda x: fqdn_name(partition, x), list_names) def to_commands(module, commands): @@ -45,3 +83,117 @@ def run_commands(module, commands, check_rc=True): module.fail_json(msg=to_text(err, errors='surrogate_then_replace'), rc=rc) responses.append(to_text(out, errors='surrogate_then_replace')) return responses + + +def cleanup_tokens(client): + try: + resource = client.api.shared.authz.tokens_s.token.load( + name=client.api.icrs.token + ) + resource.delete() + except Exception: + pass + + +class Noop(object): + """Represent no-operation required + + This class is used in the Difference engine to specify when an attribute + has not changed. Difference attributes may return an instance of this + class as a means to indicate when the attribute has not changed. + + The Noop object allows attributes to be set to None when sending updates + to the API. `None` is technically a valid value in some cases (it indicates + that the attribute should be removed from the resource). + """ + pass + + +class F5BaseClient(object): + def __init__(self, *args, **kwargs): + self.params = kwargs + + @property + def api(self): + raise F5ModuleError("Management root must be used from the concrete product classes.") + + def reconnect(self): + """Attempts to reconnect to a device + + The existing token from a ManagementRoot can become invalid if you, + for example, upgrade the device (such as is done in the *_software + module. + + This method can be used to reconnect to a remote device without + having to re-instantiate the ArgumentSpec and AnsibleF5Client classes + it will use the same values that were initially provided to those + classes + + :return: + :raises iControlUnexpectedHTTPError + """ + self.api = self.mgmt + + +class AnsibleF5Parameters(object): + def __init__(self, params=None): + self._values = defaultdict(lambda: None) + self._values['__warnings'] = [] + if params: + self.update(params=params) + + 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 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 + + def __getattr__(self, item): + # Ensures that properties that weren't defined, and therefore stashed + # in the `_values` dict, will be retrievable. + return self._values[item] + + @property + def partition(self): + if self._values['partition'] is None: + return 'Common' + return self._values['partition'].strip('/') + + @partition.setter + def partition(self, value): + self._values['partition'] = value + + def _filter_params(self, params): + return dict((k, v) for k, v in iteritems(params) if v is not None) + + +class F5ModuleError(Exception): + pass diff --git a/lib/ansible/module_utils/network/f5/iworkflow.py b/lib/ansible/module_utils/network/f5/iworkflow.py new file mode 100644 index 0000000000..96fea1dcd7 --- /dev/null +++ b/lib/ansible/module_utils/network/f5/iworkflow.py @@ -0,0 +1,31 @@ +# -*- 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 + + +try: + from f5.iworkflow import ManagementRoot + from icontrol.exceptions import iControlUnexpectedHTTPError + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + +from ansible.module_utils.network.f5.common import F5BaseClient + + +class F5Client(F5BaseClient): + @property + def api(self): + result = ManagementRoot( + self.params['server'], + self.params['user'], + self.params['password'], + port=self.params['server_port'], + verify=self.params['validate_certs'], + token='local' + ) + return result