Renames the bigip_configsync_actions module (#29747)

Retains the old name by making a symlink. We can remove it in a later
release.
This commit is contained in:
Tim Rupp 2017-09-11 21:53:44 -07:00 committed by John R Barker
parent cc343a4376
commit 74ace093b8
9 changed files with 656 additions and 385 deletions

View file

@ -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 <http://www.gnu.org/licenses/>.
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'.*(?P<msg>Recommended\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()

View file

@ -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 <http://www.gnu.org/licenses/>.
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'.*(?P<msg>Recommended\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()

View file

@ -0,0 +1 @@
bigip_configsync_action.py

View file

@ -0,0 +1,3 @@
---
device_group: "sdbt_sync_failover_dev_group"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 <http://www.gnu.org/licenses/>.
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