diff --git a/lib/ansible/modules/network/f5/bigip_configsync_action.py b/lib/ansible/modules/network/f5/bigip_configsync_action.py new file mode 100644 index 00000000000..2d20bca2c68 --- /dev/null +++ b/lib/ansible/modules/network/f5/bigip_configsync_action.py @@ -0,0 +1,389 @@ +#!/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.1' +} + +DOCUMENTATION = ''' +--- +module: bigip_configsync_action +short_description: Perform different actions related to config-sync. +description: + - Allows one to run different config-sync actions. These actions allow + you to manually sync your configuration across multiple BIG-IPs when + those devices are in an HA pair. +version_added: "2.4" +options: + device_group: + description: + - The device group that you want to perform config-sync actions on. + required: True + sync_device_to_group: + description: + - Specifies that the system synchronizes configuration data from this + device to other members of the device group. In this case, the device + will do a "push" to all the other devices in the group. This option + is mutually exclusive with the C(sync_group_to_device) option. + choices: + - yes + - no + sync_most_recent_to_device: + description: + - Specifies that the system synchronizes configuration data from the + device with the most recent configuration. In this case, the device + will do a "pull" from the most recently updated device. This option + is mutually exclusive with the C(sync_device_to_group) options. + choices: + - yes + - no + overwrite_config: + description: + - Indicates that the sync operation overwrites the configuration on + the target. + default: no + choices: + - yes + - no +notes: + - Requires the f5-sdk Python package on the host. This is as easy as pip + install f5-sdk. + - Requires the objectpath Python package on the host. This is as easy as pip + install objectpath. +requirements: + - f5-sdk >= 2.2.3 +extends_documentation_fragment: f5 +author: + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Sync configuration from device to group + bigip_configsync_actions: + device_group: "foo-group" + sync_device_to_group: yes + server: "lb01.mydomain.com" + user: "admin" + password: "secret" + validate_certs: no + delegate_to: localhost + +- name: Sync configuration from most recent device to the current host + bigip_configsync_actions: + device_group: "foo-group" + sync_most_recent_to_device: yes + server: "lb01.mydomain.com" + user: "admin" + password: "secret" + validate_certs: no + delegate_to: localhost + +- name: Perform an initial sync of a device to a new device group + bigip_configsync_actions: + device_group: "new-device-group" + sync_device_to_group: yes + server: "lb01.mydomain.com" + user: "admin" + password: "secret" + validate_certs: no + delegate_to: localhost +''' + +RETURN = ''' +# only common fields returned +''' + +import time +import re + +try: + from objectpath import Tree + HAS_OBJPATH = True +except ImportError: + HAS_OBJPATH = False + +from ansible.module_utils.basic import BOOLEANS_TRUE +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 + +try: + from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError +except ImportError: + HAS_F5SDK = False + + +class Parameters(AnsibleF5Parameters): + api_attributes = [] + returnables = [] + + @property + def direction(self): + if self.sync_device_to_group: + return 'to-group' + else: + return 'from-group' + + @property + def sync_device_to_group(self): + result = self._cast_to_bool(self._values['sync_device_to_group']) + return result + + @property + def sync_group_to_device(self): + result = self._cast_to_bool(self._values['sync_group_to_device']) + return result + + @property + def force_full_push(self): + if self.overwrite_config: + return 'force-full-load-push' + else: + return '' + + @property + def overwrite_config(self): + result = self._cast_to_bool(self._values['overwrite_config']) + return result + + def _cast_to_bool(self, value): + if value is None: + return None + elif value in BOOLEANS_TRUE: + return True + else: + return False + + 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 + + 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 + + +class ModuleManager(object): + def __init__(self, client): + self.client = client + self.want = Parameters(self.client.module.params) + + def exec_module(self): + result = dict() + + try: + changed = self.present() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + result.update(dict(changed=changed)) + return result + + def present(self): + if not self._device_group_exists(): + raise F5ModuleError( + "The specified 'device_group' not not exist." + ) + if self._sync_to_group_required(): + raise F5ModuleError( + "This device group needs an initial sync. Please use " + "'sync_device_to_group'" + ) + if self.exists(): + return False + else: + return self.execute() + + def _sync_to_group_required(self): + resource = self.read_current_from_device() + status = self._get_status_from_resource(resource) + if status == 'Awaiting Initial Sync' and self.want.sync_group_to_device: + return True + return False + + def _device_group_exists(self): + result = self.client.api.tm.cm.device_groups.device_group.exists( + name=self.want.device_group + ) + return result + + def execute(self): + self.execute_on_device() + self._wait_for_sync() + return True + + def exists(self): + resource = self.read_current_from_device() + status = self._get_status_from_resource(resource) + if status == 'In Sync': + return True + else: + return False + + def execute_on_device(self): + sync_cmd = 'config-sync {0} {1} {2}'.format( + self.want.direction, + self.want.device_group, + self.want.force_full_push + ) + self.client.api.tm.cm.exec_cmd( + 'run', + utilCmdArgs=sync_cmd + ) + + def _wait_for_sync(self): + # Wait no more than half an hour + resource = self.read_current_from_device() + for x in range(1, 180): + time.sleep(3) + status = self._get_status_from_resource(resource) + + # Changes Pending: + # The existing device has changes made to it that + # need to be sync'd to the group. + # + # Awaiting Initial Sync: + # This is a new device group and has not had any sync + # done yet. You _must_ `sync_device_to_group` in this + # case. + # + # Not All Devices Synced: + # A device group will go into this state immediately + # after starting the sync and stay until all devices finish. + # + if status in ['Changes Pending']: + details = self._get_details_from_resource(resource) + self._validate_pending_status(details) + pass + elif status in ['Awaiting Initial Sync', 'Not All Devices Synced']: + pass + elif status == 'In Sync': + return + else: + raise F5ModuleError(status) + + def read_current_from_device(self): + result = self.client.api.tm.cm.sync_status.load() + return result + + def _get_status_from_resource(self, resource): + resource.refresh() + entries = resource.entries.copy() + k, v = entries.popitem() + status = v['nestedStats']['entries']['status']['description'] + return status + + def _get_details_from_resource(self, resource): + resource.refresh() + stats = resource.entries.copy() + tree = Tree(stats) + details = list(tree.execute('$..*["details"]["description"]')) + result = details[::-1] + return result + + def _validate_pending_status(self, details): + """Validate the content of a pending sync operation + + This is a hack. The REST API is not consistent with its 'status' values + so this method is here to check the returned strings from the operation + and see if it reported any of these inconsistencies. + + :param details: + :raises F5ModuleError: + """ + pattern1 = r'.*(?PRecommended\s+action.*)' + for detail in details: + matches = re.search(pattern1, detail) + if matches: + raise F5ModuleError(matches.group('msg')) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.argument_spec = dict( + sync_device_to_group=dict( + type='bool' + ), + sync_most_recent_to_device=dict( + type='bool' + ), + overwrite_config=dict( + type='bool', + default='no' + ), + device_group=dict( + required=True + ) + ) + self.f5_product_name = 'bigip' + self.required_one_of = [ + ['sync_device_to_group', 'sync_most_recent_to_device'] + ] + self.mutually_exclusive = [ + ['sync_device_to_group', 'sync_most_recent_to_device'] + ] + self.required_one_of = [ + ['sync_device_to_group', 'sync_most_recent_to_device'] + ] + + +def main(): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + if not HAS_OBJPATH: + raise F5ModuleError("The python objectpath module is required") + + spec = ArgumentSpec() + + client = AnsibleF5Client( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + mutually_exclusive=spec.mutually_exclusive, + required_one_of=spec.required_one_of, + 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/lib/ansible/modules/network/f5/bigip_configsync_actions.py b/lib/ansible/modules/network/f5/bigip_configsync_actions.py deleted file mode 100644 index d354f529f15..00000000000 --- a/lib/ansible/modules/network/f5/bigip_configsync_actions.py +++ /dev/null @@ -1,385 +0,0 @@ -#!/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 = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: bigip_configsync_actions -short_description: Perform different actions related to config-sync. -description: - - Allows one to run different config-sync actions. These actions allow - you to manually sync your configuration across multiple BIG-IPs when - those devices are in an HA pair. -version_added: "2.4" -options: - device_group: - description: - - The device group that you want to perform config-sync actions on. - required: True - sync_device_to_group: - description: - - Specifies that the system synchronizes configuration data from this - device to other members of the device group. In this case, the device - will do a "push" to all the other devices in the group. This option - is mutually exclusive with the C(sync_group_to_device) option. - choices: - - yes - - no - sync_most_recent_to_device: - description: - - Specifies that the system synchronizes configuration data from the - device with the most recent configuration. In this case, the device - will do a "pull" from the most recently updated device. This option - is mutually exclusive with the C(sync_device_to_group) options. - choices: - - yes - - no - overwrite_config: - description: - - Indicates that the sync operation overwrites the configuration on - the target. - default: no - choices: - - yes - - no -notes: - - Requires the f5-sdk Python package on the host. This is as easy as pip - install f5-sdk. - - Requires the objectpath Python package on the host. This is as easy as pip - install objectpath. -requirements: - - f5-sdk >= 2.2.3 -extends_documentation_fragment: f5 -author: - - Tim Rupp (@caphrim007) -''' - -EXAMPLES = ''' -- name: Sync configuration from device to group - bigip_configsync_actions: - device_group: "foo-group" - sync_device_to_group: yes - server: "lb01.mydomain.com" - user: "admin" - password: "secret" - validate_certs: no - delegate_to: localhost - -- name: Sync configuration from most recent device to the current host - bigip_configsync_actions: - device_group: "foo-group" - sync_most_recent_to_device: yes - server: "lb01.mydomain.com" - user: "admin" - password: "secret" - validate_certs: no - delegate_to: localhost - -- name: Perform an initial sync of a device to a new device group - bigip_configsync_actions: - device_group: "new-device-group" - sync_device_to_group: yes - server: "lb01.mydomain.com" - user: "admin" - password: "secret" - validate_certs: no - delegate_to: localhost -''' - -RETURN = ''' -# only common fields returned -''' - -import time -import re - -try: - from objectpath import Tree - HAS_OBJPATH = True -except ImportError: - HAS_OBJPATH = False - -from ansible.module_utils.basic import BOOLEANS_TRUE -from ansible.module_utils.f5_utils import ( - AnsibleF5Client, - AnsibleF5Parameters, - HAS_F5SDK, - F5ModuleError, - iControlUnexpectedHTTPError -) - - -class Parameters(AnsibleF5Parameters): - api_attributes = [] - returnables = [] - - @property - def direction(self): - if self.sync_device_to_group: - return 'to-group' - else: - return 'from-group' - - @property - def sync_device_to_group(self): - result = self._cast_to_bool(self._values['sync_device_to_group']) - return result - - @property - def sync_group_to_device(self): - result = self._cast_to_bool(self._values['sync_group_to_device']) - return result - - @property - def force_full_push(self): - if self.overwrite_config: - return 'force-full-load-push' - else: - return '' - - @property - def overwrite_config(self): - result = self._cast_to_bool(self._values['overwrite_config']) - return result - - def _cast_to_bool(self, value): - if value is None: - return None - elif value in BOOLEANS_TRUE: - return True - else: - return False - - 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 - - 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 - - -class ModuleManager(object): - def __init__(self, client): - self.client = client - self.want = Parameters(self.client.module.params) - - def exec_module(self): - result = dict() - - try: - changed = self.present() - except iControlUnexpectedHTTPError as e: - raise F5ModuleError(str(e)) - - result.update(dict(changed=changed)) - return result - - def present(self): - if not self._device_group_exists(): - raise F5ModuleError( - "The specified 'device_group' not not exist." - ) - if self._sync_to_group_required(): - raise F5ModuleError( - "This device group needs an initial sync. Please use " - "'sync_device_to_group'" - ) - if self.exists(): - return False - else: - return self.execute() - - def _sync_to_group_required(self): - resource = self.read_current_from_device() - status = self._get_status_from_resource(resource) - if status == 'Awaiting Initial Sync' and self.want.sync_group_to_device: - return True - return False - - def _device_group_exists(self): - result = self.client.api.tm.cm.device_groups.device_group.exists( - name=self.want.device_group - ) - return result - - def execute(self): - self.execute_on_device() - self._wait_for_sync() - return True - - def exists(self): - resource = self.read_current_from_device() - status = self._get_status_from_resource(resource) - if status == 'In Sync': - return True - else: - return False - - def execute_on_device(self): - sync_cmd = 'config-sync {0} {1} {2}'.format( - self.want.direction, - self.want.device_group, - self.want.force_full_push - ) - self.client.api.tm.cm.exec_cmd( - 'run', - utilCmdArgs=sync_cmd - ) - - def _wait_for_sync(self): - # Wait no more than half an hour - resource = self.read_current_from_device() - for x in range(1, 180): - time.sleep(3) - status = self._get_status_from_resource(resource) - - # Changes Pending: - # The existing device has changes made to it that - # need to be sync'd to the group. - # - # Awaiting Initial Sync: - # This is a new device group and has not had any sync - # done yet. You _must_ `sync_device_to_group` in this - # case. - # - # Not All Devices Synced: - # A device group will go into this state immediately - # after starting the sync and stay until all devices finish. - # - if status in ['Changes Pending']: - details = self._get_details_from_resource(resource) - self._validate_pending_status(details) - pass - elif status in ['Awaiting Initial Sync', 'Not All Devices Synced']: - pass - elif status == 'In Sync': - return - else: - raise F5ModuleError(status) - - def read_current_from_device(self): - result = self.client.api.tm.cm.sync_status.load() - return result - - def _get_status_from_resource(self, resource): - resource.refresh() - entries = resource.entries.copy() - k, v = entries.popitem() - status = v['nestedStats']['entries']['status']['description'] - return status - - def _get_details_from_resource(self, resource): - resource.refresh() - stats = resource.entries.copy() - tree = Tree(stats) - details = list(tree.execute('$..*["details"]["description"]')) - result = details[::-1] - return result - - def _validate_pending_status(self, details): - """Validate the content of a pending sync operation - - This is a hack. The REST API is not consistent with its 'status' values - so this method is here to check the returned strings from the operation - and see if it reported any of these inconsistencies. - - :param details: - :raises F5ModuleError: - """ - pattern1 = r'.*(?PRecommended\s+action.*)' - for detail in details: - matches = re.search(pattern1, detail) - if matches: - raise F5ModuleError(matches.group('msg')) - - -class ArgumentSpec(object): - def __init__(self): - self.supports_check_mode = True - self.argument_spec = dict( - sync_device_to_group=dict( - type='bool' - ), - sync_most_recent_to_device=dict( - type='bool' - ), - overwrite_config=dict( - type='bool', - default='no' - ), - device_group=dict( - required=True - ) - ) - self.f5_product_name = 'bigip' - self.required_one_of = [ - ['sync_device_to_group', 'sync_most_recent_to_device'] - ] - self.mutually_exclusive = [ - ['sync_device_to_group', 'sync_most_recent_to_device'] - ] - self.required_one_of = [ - ['sync_device_to_group', 'sync_most_recent_to_device'] - ] - - -def main(): - if not HAS_F5SDK: - raise F5ModuleError("The python f5-sdk module is required") - - if not HAS_OBJPATH: - raise F5ModuleError("The python objectpath module is required") - - spec = ArgumentSpec() - - client = AnsibleF5Client( - argument_spec=spec.argument_spec, - supports_check_mode=spec.supports_check_mode, - mutually_exclusive=spec.mutually_exclusive, - required_one_of=spec.required_one_of, - 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/lib/ansible/modules/network/f5/bigip_configsync_actions.py b/lib/ansible/modules/network/f5/bigip_configsync_actions.py new file mode 120000 index 00000000000..3f9d948c52b --- /dev/null +++ b/lib/ansible/modules/network/f5/bigip_configsync_actions.py @@ -0,0 +1 @@ +bigip_configsync_action.py \ No newline at end of file diff --git a/test/integration/targets/bigip_configsync_action/defaults/main.yaml b/test/integration/targets/bigip_configsync_action/defaults/main.yaml new file mode 100644 index 00000000000..06f1a50d56b --- /dev/null +++ b/test/integration/targets/bigip_configsync_action/defaults/main.yaml @@ -0,0 +1,3 @@ +--- + +device_group: "sdbt_sync_failover_dev_group" diff --git a/test/integration/targets/bigip_configsync_action/tasks/main.yaml b/test/integration/targets/bigip_configsync_action/tasks/main.yaml new file mode 100644 index 00000000000..c43cee2fd24 --- /dev/null +++ b/test/integration/targets/bigip_configsync_action/tasks/main.yaml @@ -0,0 +1,16 @@ +--- + +# In this task list, the 0th item is the Active unit and the 1st item is the +# standby unit. + +- include: setup.yaml + when: ansible_play_batch[0] == inventory_hostname + +- include: test-device-to-group.yaml + when: ansible_play_batch[0] == inventory_hostname + +- include: test-pull-recent-device.yaml + when: ansible_play_batch[1] == inventory_hostname + +- include: teardown.yaml + when: ansible_play_batch[0] == inventory_hostname diff --git a/test/integration/targets/bigip_configsync_action/tasks/setup.yaml b/test/integration/targets/bigip_configsync_action/tasks/setup.yaml new file mode 100644 index 00000000000..5278e10da03 --- /dev/null +++ b/test/integration/targets/bigip_configsync_action/tasks/setup.yaml @@ -0,0 +1,13 @@ +--- + +- name: Create pool + bigip_pool: + lb_method: "round-robin" + name: "cs1.pool" + state: "present" + register: result + +- name: Assert Create pool + assert: + that: + - result|changed diff --git a/test/integration/targets/bigip_configsync_action/tasks/teardown.yaml b/test/integration/targets/bigip_configsync_action/tasks/teardown.yaml new file mode 100644 index 00000000000..d0672c14309 --- /dev/null +++ b/test/integration/targets/bigip_configsync_action/tasks/teardown.yaml @@ -0,0 +1,21 @@ +--- + +- name: Delete pool - First device + bigip_pool: + name: "{{ item }}" + state: "absent" + with_items: + - "cs1.pool" + - "cs2.pool" + register: result + +- name: Assert Delete pool + assert: + that: + - result|changed + +- name: Sync configuration from device to group + bigip_configsync_actions: + device_group: "{{ device_group }}" + sync_device_to_group: yes + register: result diff --git a/test/integration/targets/bigip_configsync_action/tasks/test-device-to-group.yaml b/test/integration/targets/bigip_configsync_action/tasks/test-device-to-group.yaml new file mode 100644 index 00000000000..8860af69e5e --- /dev/null +++ b/test/integration/targets/bigip_configsync_action/tasks/test-device-to-group.yaml @@ -0,0 +1,23 @@ +--- + +- name: Sync configuration from device to group + bigip_configsync_actions: + device_group: "{{ device_group }}" + sync_device_to_group: yes + register: result + +- name: Sync configuration from device to group + assert: + that: + - result|changed + +- name: Sync configuration from device to group - Idempotent check + bigip_configsync_actions: + device_group: "{{ device_group }}" + sync_device_to_group: yes + register: result + +- name: Sync configuration from device to group - Idempotent check + assert: + that: + - not result|changed diff --git a/test/integration/targets/bigip_configsync_action/tasks/test-pull-recent-device.yaml b/test/integration/targets/bigip_configsync_action/tasks/test-pull-recent-device.yaml new file mode 100644 index 00000000000..f077322a504 --- /dev/null +++ b/test/integration/targets/bigip_configsync_action/tasks/test-pull-recent-device.yaml @@ -0,0 +1,48 @@ +--- + +- name: Create another pool - First device + bigip_pool: + server: "{{ hostvars['bigip1']['ansible_host'] }}" + lb_method: "round_robin" + name: "cs2.pool" + state: "present" + register: result + +- name: Assert Create another pool - First device + assert: + that: + - result|changed + +- name: Sync configuration from most recent - Second device + bigip_configsync_actions: + device_group: "{{ device_group }}" + sync_most_recent_to_device: yes + register: result + +- name: Assert Sync configuration from most recent - Second device + assert: + that: + - result|changed + +- name: Sync configuration from most recent - Second device - Idempotent check + bigip_configsync_actions: + device_group: "{{ device_group }}" + sync_most_recent_to_device: yes + register: result + +- name: Assert Sync configuration from most recent - Second device - Idempotent check + assert: + that: + - not result|changed + +- name: Create another pool again - Second device - ensure it was created in previous sync + bigip_pool: + lb_method: "round_robin" + name: "cs2.pool" + state: "present" + register: result + +- name: Assert Create another pool again - Second device - ensure it was deleted in previous sync + assert: + that: + - not result|changed diff --git a/test/units/modules/network/f5/test_bigip_configsync_action.py b/test/units/modules/network/f5/test_bigip_configsync_action.py new file mode 100644 index 00000000000..c1df7c80db2 --- /dev/null +++ b/test/units/modules/network/f5/test_bigip_configsync_action.py @@ -0,0 +1,142 @@ +# -*- 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_configsync_actions import Parameters + from library.bigip_configsync_actions import ModuleManager + from library.bigip_configsync_actions import ArgumentSpec + from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError +except ImportError: + try: + from ansible.modules.network.f5.bigip_configsync_actions import Parameters + from ansible.modules.network.f5.bigip_configsync_actions import ModuleManager + from ansible.modules.network.f5.bigip_configsync_actions import ArgumentSpec + from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError + 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(self): + args = dict( + sync_device_to_group=True, + sync_group_to_device=True, + overwrite_config=True, + device_group="foo" + ) + p = Parameters(args) + assert p.sync_device_to_group is True + assert p.sync_group_to_device is True + assert p.overwrite_config is True + assert p.device_group == 'foo' + + def test_module_parameters_yes_no(self): + args = dict( + sync_device_to_group='yes', + sync_group_to_device='no', + overwrite_config='yes', + device_group="foo" + ) + p = Parameters(args) + assert p.sync_device_to_group is True + assert p.sync_group_to_device is False + assert p.overwrite_config is True + assert p.device_group == 'foo' + + +@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_update_agent_status_traps(self, *args): + set_module_args(dict( + sync_device_to_group='yes', + device_group="foo", + 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._device_group_exists = Mock(return_value=True) + mm._sync_to_group_required = Mock(return_value=False) + mm.execute_on_device = Mock(return_value=True) + mm.read_current_from_device = Mock(return_value=None) + + mm._get_status_from_resource = Mock() + mm._get_status_from_resource.side_effect = [ + 'Changes Pending', 'Awaiting Initial Sync', 'In Sync' + ] + + results = mm.exec_module() + + assert results['changed'] is True