Refactors the gtm datacenter module (#33474)

* Refactors the gtm datacenter module

This module needed to be inline with current f5 coding standards.
This fixes that

* Fixes upstream errors
This commit is contained in:
Tim Rupp 2017-12-01 20:44:39 -08:00 committed by GitHub
parent 76135a500e
commit e1062d1a7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 618 additions and 272 deletions

View file

@ -1,29 +1,18 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright 2016 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/>.
# 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_gtm_datacenter
short_description: Manage Datacenter configuration in BIG-IP
@ -31,7 +20,7 @@ description:
- Manage BIG-IP data center configuration. A data center defines the location
where the physical network components reside, such as the server and link
objects that share the same subnet on the network. This module is able to
manipulate the data center definitions in a BIG-IP
manipulate the data center definitions in a BIG-IP.
version_added: "2.2"
options:
contact:
@ -44,6 +33,8 @@ options:
description:
- Whether the data center should be enabled. At least one of C(state) and
C(enabled) are required.
- Deprecated in 2.4. Use C(state) and either C(enabled) or C(disabled)
instead.
choices:
- yes
- no
@ -53,17 +44,22 @@ options:
name:
description:
- The name of the data center.
required: true
required: True
state:
description:
- The state of the datacenter on the BIG-IP. When C(present), guarantees
that the data center exists. When C(absent) removes the data center
from the BIG-IP. C(enabled) will enable the data center and C(disabled)
will ensure the data center is disabled. At least one of state and
enabled are required.
- The virtual address state. If C(absent), an attempt to delete the
virtual address will be made. This will only succeed if this
virtual address is not in use by a virtual server. C(present) creates
the virtual address and enables it. If C(enabled), enable the virtual
address if it exists. If C(disabled), create the virtual address if
needed, and set state to C(disabled).
required: False
default: present
choices:
- present
- absent
- enabled
- disabled
notes:
- Requires the f5-sdk Python package on the host. This is as easy as
pip install f5-sdk.
@ -74,301 +70,357 @@ author:
- Tim Rupp (@caphrim007)
'''
EXAMPLES = '''
EXAMPLES = r'''
- name: Create data center "New York"
bigip_gtm_datacenter:
server: "big-ip"
name: "New York"
location: "222 West 23rd"
server: lb.mydomain.com
user: admin
password: secret
name: New York
location: 222 West 23rd
delegate_to: localhost
'''
RETURN = '''
RETURN = r'''
contact:
description: The contact that was set on the datacenter
returned: changed
type: string
sample: "admin@root.local"
description: The contact that was set on the datacenter.
returned: changed
type: string
sample: admin@root.local
description:
description: The description that was set for the datacenter
returned: changed
type: string
sample: "Datacenter in NYC"
description: The description that was set for the datacenter.
returned: changed
type: string
sample: Datacenter in NYC
enabled:
description: Whether the datacenter is enabled or not
returned: changed
type: bool
sample: true
description: Whether the datacenter is enabled or not
returned: changed
type: bool
sample: true
location:
description: The location that is set for the datacenter
returned: changed
type: string
sample: "222 West 23rd"
name:
description: Name of the datacenter being manipulated
returned: changed
type: string
sample: "foo"
description: The location that is set for the datacenter.
returned: changed
type: string
sample: 222 West 23rd
'''
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE
from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE
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 f5.bigip import ManagementRoot
from icontrol.session import iControlUnexpectedHTTPError
HAS_F5SDK = True
from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError
except ImportError:
HAS_F5SDK = False
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.ec2 import camel_dict_to_snake_dict
from ansible.module_utils.f5_utils import F5ModuleError, f5_argument_spec
class Parameters(AnsibleF5Parameters):
api_map = {}
class BigIpGtmDatacenter(object):
def __init__(self, *args, **kwargs):
if not HAS_F5SDK:
raise F5ModuleError("The python f5-sdk module is required")
updatables = [
'location', 'description', 'contact',
# The params that change in the module
self.cparams = dict()
# TODO: Remove this method in v2.5
'enabled'
]
# Stores the params that are sent to the module
self.params = kwargs
self.api = ManagementRoot(kwargs['server'],
kwargs['user'],
kwargs['password'],
port=kwargs['server_port'])
returnables = [
'location', 'description', 'contact',
def create(self):
params = dict()
# TODO: Remove this method in v2.5
'enabled'
]
check_mode = self.params['check_mode']
contact = self.params['contact']
description = self.params['description']
location = self.params['location']
name = self.params['name']
partition = self.params['partition']
enabled = self.params['enabled']
api_attributes = [
'enabled', 'location', 'description', 'contact'
]
# Specifically check for None because a person could supply empty
# values which would technically still be valid
if contact is not None:
params['contact'] = contact
if description is not None:
params['description'] = description
if location is not None:
params['location'] = location
if enabled is not None:
params['enabled'] = True
else:
params['disabled'] = False
params['name'] = name
params['partition'] = partition
self.cparams = camel_dict_to_snake_dict(params)
if check_mode:
@property
def disabled(self):
if self._values['state'] == 'disabled':
return True
d = self.api.tm.gtm.datacenters.datacenter
d.create(**params)
if not self.exists():
raise F5ModuleError("Failed to create the datacenter")
return True
def read(self):
"""Read information and transform it
The values that are returned by BIG-IP in the f5-sdk can have encoding
attached to them as well as be completely missing in some cases.
Therefore, this method will transform the data from the BIG-IP into a
format that is more easily consumable by the rest of the class and the
parameters that are supported by the module.
"""
p = dict()
name = self.params['name']
partition = self.params['partition']
r = self.api.tm.gtm.datacenters.datacenter.load(
name=name,
partition=partition
)
if hasattr(r, 'servers'):
# Deliberately using sets to suppress duplicates
p['servers'] = set([str(x) for x in r.servers])
if hasattr(r, 'contact'):
p['contact'] = str(r.contact)
if hasattr(r, 'location'):
p['location'] = str(r.location)
if hasattr(r, 'description'):
p['description'] = str(r.description)
if r.enabled:
p['enabled'] = True
else:
p['enabled'] = False
p['name'] = name
return p
def update(self):
changed = False
params = dict()
current = self.read()
check_mode = self.params['check_mode']
contact = self.params['contact']
description = self.params['description']
location = self.params['location']
name = self.params['name']
partition = self.params['partition']
enabled = self.params['enabled']
if contact is not None:
if 'contact' in current:
if contact != current['contact']:
params['contact'] = contact
else:
params['contact'] = contact
if description is not None:
if 'description' in current:
if description != current['description']:
params['description'] = description
else:
params['description'] = description
if location is not None:
if 'location' in current:
if location != current['location']:
params['location'] = location
else:
params['location'] = location
if enabled is not None:
if current['enabled'] != enabled:
if enabled is True:
params['enabled'] = True
params['disabled'] = False
else:
params['disabled'] = True
params['enabled'] = False
if params:
changed = True
if check_mode:
return changed
self.cparams = camel_dict_to_snake_dict(params)
else:
return changed
r = self.api.tm.gtm.datacenters.datacenter.load(
name=name,
partition=partition
)
r.update(**params)
r.refresh()
return True
def delete(self):
params = dict()
check_mode = self.params['check_mode']
params['name'] = self.params['name']
params['partition'] = self.params['partition']
self.cparams = camel_dict_to_snake_dict(params)
if check_mode:
# TODO: Remove this method in v2.5
elif self._values['disabled'] in BOOLEANS_TRUE:
return True
dc = self.api.tm.gtm.datacenters.datacenter.load(**params)
dc.delete()
if self.exists():
raise F5ModuleError("Failed to delete the datacenter")
return True
def present(self):
changed = False
if self.exists():
changed = self.update()
# TODO: Remove this method in v2.5
elif self._values['disabled'] in BOOLEANS_FALSE:
return False
# TODO: Remove this method in v2.5
elif self._values['enabled'] in BOOLEANS_FALSE:
return True
# TODO: Remove this method in v2.5
elif self._values['enabled'] in BOOLEANS_TRUE:
return False
elif self._values['state'] == 'enabled':
return False
else:
changed = self.create()
return None
return changed
@property
def enabled(self):
if self._values['state'] == 'enabled':
return True
# TODO: Remove this method in v2.5
elif self._values['enabled'] in BOOLEANS_TRUE:
return True
# TODO: Remove this method in v2.5
elif self._values['enabled'] in BOOLEANS_FALSE:
return False
# TODO: Remove this method in v2.5
elif self._values['disabled'] in BOOLEANS_FALSE:
return True
# TODO: Remove this method in v2.5
elif self._values['disabled'] in BOOLEANS_TRUE:
return False
elif self._values['state'] == 'disabled':
return False
else:
return None
def absent(self):
@property
def state(self):
if self.enabled and self._values['state'] != 'present':
return 'enabled'
elif self.disabled and self._values['state'] != 'present':
return 'disabled'
else:
return self._values['state']
# TODO: Remove this method in v2.5
@state.setter
def state(self, value):
self._values['state'] = value
# Only do this if not using legacy params
if self._values['enabled'] is None:
if self._values['state'] in ['enabled', 'present']:
self._values['enabled'] = True
self._values['disabled'] = False
elif self._values['state'] == 'disabled':
self._values['enabled'] = False
self._values['disabled'] = True
else:
if self._values['__warnings'] is None:
self._values['__warnings'] = []
self._values['__warnings'].append(
dict(
msg="Usage of the 'enabled' parameter is deprecated",
version='2.4'
)
)
def to_return(self):
result = {}
for returnable in self.returnables:
result[returnable] = getattr(self, returnable)
result = self._filter_params(result)
return result
def api_params(self):
result = {}
for api_attribute in self.api_attributes:
if 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 Changes(Parameters):
@property
def enabled(self):
if self._values['enabled'] in BOOLEANS_TRUE:
return True
else:
return False
class ModuleManager(object):
def __init__(self, client):
self.client = client
self.have = None
self.want = Parameters(self.client.module.params)
self.changes = Changes()
def _set_changed_options(self):
changed = {}
for key in Parameters.returnables:
if getattr(self.want, key) is not None:
changed[key] = getattr(self.want, key)
if 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] = attr1
if changed:
self.changes = Changes(changed)
return True
return False
def exec_module(self):
changed = False
if self.exists():
changed = self.delete()
return changed
def exists(self):
name = self.params['name']
partition = self.params['partition']
return self.api.tm.gtm.datacenters.datacenter.exists(
name=name,
partition=partition
)
def flush(self):
result = dict()
state = self.params['state']
enabled = self.params['enabled']
if state is None and enabled is None:
raise F5ModuleError("Neither 'state' nor 'enabled' set")
state = self.want.state
try:
if state == "present":
if state in ['present', 'enabled', 'disabled']:
changed = self.present()
# Ensure that this field is not returned to the user since it
# is not a valid parameter to the module.
if 'disabled' in self.cparams:
del self.cparams['disabled']
elif state == "absent":
changed = self.absent()
except iControlUnexpectedHTTPError as e:
raise F5ModuleError(str(e))
result.update(**self.cparams)
changes = self.changes.to_return()
result.update(**changes)
result.update(dict(changed=changed))
self._announce_deprecations()
return result
def _announce_deprecations(self):
warnings = []
if self.want:
warnings += self.want._values.get('__warnings', [])
if self.have:
warnings += self.have._values.get('__warnings', [])
for warning in warnings:
self.client.module.deprecate(
msg=warning['msg'],
version=warning['version']
)
def should_update(self):
result = self._update_changed_options()
if result:
return True
return False
def present(self):
if self.exists():
return self.update()
else:
return self.create()
def absent(self):
changed = False
if self.exists():
changed = self.remove()
return changed
def read_current_from_device(self):
resource = self.client.api.tm.gtm.datacenters.datacenter.load(
name=self.want.name,
partition=self.want.partition
)
result = resource.attrs
return Parameters(result)
def exists(self):
result = self.client.api.tm.gtm.datacenters.datacenter.exists(
name=self.want.name,
partition=self.want.partition
)
return result
def update(self):
self.have = self.read_current_from_device()
if not self.should_update():
return False
if self.client.check_mode:
return True
self.update_on_device()
return True
def update_on_device(self):
params = self.want.api_params()
resource = self.client.api.tm.gtm.datacenters.datacenter.load(
name=self.want.name,
partition=self.want.partition
)
resource.modify(**params)
def create(self):
self._set_changed_options()
if self.client.check_mode:
return True
self.create_on_device()
if self.exists():
return True
else:
raise F5ModuleError("Failed to create the datacenter")
def create_on_device(self):
params = self.want.api_params()
self.client.api.tm.gtm.datacenters.datacenter.create(
name=self.want.name,
partition=self.want.partition,
**params
)
def remove(self):
if self.client.check_mode:
return True
self.remove_from_device()
if self.exists():
raise F5ModuleError("Failed to delete the datacenter")
return True
def remove_from_device(self):
resource = self.client.api.tm.gtm.datacenters.datacenter.load(
name=self.want.name,
partition=self.want.partition
)
resource.delete()
class ArgumentSpec(object):
def __init__(self):
self.supports_check_mode = True
self.argument_spec = dict(
contact=dict(),
description=dict(),
enabled=dict(
type='bool',
),
location=dict(),
name=dict(required=True),
state=dict(
type='str',
default='present',
choices=['present', 'absent', 'disabled', 'enabled']
)
)
self.f5_product_name = 'bigip'
def main():
argument_spec = f5_argument_spec()
if not HAS_F5SDK:
raise F5ModuleError("The python f5-sdk module is required")
meta_args = dict(
contact=dict(required=False, default=None),
description=dict(required=False, default=None),
enabled=dict(required=False, type='bool', default=None),
location=dict(required=False, default=None),
name=dict(required=True)
)
argument_spec.update(meta_args)
spec = ArgumentSpec()
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True
client = AnsibleF5Client(
argument_spec=spec.argument_spec,
supports_check_mode=spec.supports_check_mode,
f5_product_name=spec.f5_product_name
)
try:
obj = BigIpGtmDatacenter(check_mode=module.check_mode, **module.params)
result = obj.flush()
module.exit_json(**result)
mm = ModuleManager(client)
results = mm.exec_module()
client.module.exit_json(**results)
except F5ModuleError as e:
module.fail_json(msg=str(e))
client.module.fail_json(msg=str(e))
if __name__ == '__main__':
main()

View file

@ -0,0 +1,9 @@
{
"kind": "tm:gtm:datacenter:datacenterstate",
"name": "asd",
"partition": "Common",
"fullPath": "/Common/asd",
"generation": 278,
"selfLink": "https://localhost/mgmt/tm/gtm/datacenter/~Common~asd?ver=12.1.2",
"enabled": true
}

View file

@ -0,0 +1,12 @@
{
"kind": "tm:gtm:datacenter:datacenterstate",
"name": "foo",
"partition": "Common",
"fullPath": "/Common/foo",
"generation": 303,
"selfLink": "https://localhost/mgmt/tm/gtm/datacenter/~Common~foo?ver=12.1.2",
"contact": "admin@root.local",
"description": "This is a foo description",
"disabled": true,
"location": "New York"
}

View file

@ -0,0 +1,273 @@
# -*- 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
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 Mock
from ansible.compat.tests.mock import patch
from ansible.module_utils.f5_utils import AnsibleF5Client
try:
from library.bigip_gtm_datacenter import Parameters
from library.bigip_gtm_datacenter import ModuleManager
from library.bigip_gtm_datacenter import ArgumentSpec
from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError
from test.unit.modules.utils import set_module_args
except ImportError:
try:
from ansible.modules.network.f5.bigip_gtm_datacenter import Parameters
from ansible.modules.network.f5.bigip_gtm_datacenter import ModuleManager
from ansible.modules.network.f5.bigip_gtm_datacenter import ArgumentSpec
from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError
from units.modules.utils import set_module_args
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 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(
state='present',
contact='foo',
description='bar',
location='baz',
name='datacenter'
)
p = Parameters(args)
assert p.state == 'present'
def test_api_parameters(self):
args = load_fixture('load_gtm_datacenter_default.json')
p = Parameters(args)
assert p.name == 'asd'
def test_module_parameters_state_present(self):
args = dict(
state='present'
)
p = Parameters(args)
assert p.state == 'present'
assert p.enabled is True
def test_module_parameters_state_absent(self):
args = dict(
state='absent'
)
p = Parameters(args)
assert p.state == 'absent'
def test_module_parameters_state_enabled(self):
args = dict(
state='enabled'
)
p = Parameters(args)
assert p.state == 'enabled'
assert p.enabled is True
assert p.disabled is False
def test_module_parameters_state_disabled(self):
args = dict(
state='disabled'
)
p = Parameters(args)
assert p.state == 'disabled'
assert p.enabled is False
assert p.disabled is True
@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_create_datacenter(self, *args):
set_module_args(dict(
state='present',
password='admin',
server='localhost',
user='admin',
name='foo'
))
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.exists = Mock(side_effect=[False, True])
mm.create_on_device = Mock(return_value=True)
results = mm.exec_module()
assert results['changed'] is True
def test_create_disabled_datacenter(self, *args):
set_module_args(dict(
state='disabled',
password='admin',
server='localhost',
user='admin',
name='foo'
))
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.exists = Mock(side_effect=[False, True])
mm.create_on_device = Mock(return_value=True)
results = mm.exec_module()
assert results['changed'] is True
assert results['enabled'] is False
def test_create_enabled_datacenter(self, *args):
set_module_args(dict(
state='enabled',
password='admin',
server='localhost',
user='admin',
name='foo'
))
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.exists = Mock(side_effect=[False, True])
mm.create_on_device = Mock(return_value=True)
results = mm.exec_module()
assert results['changed'] is True
assert results['enabled'] is True
def test_idempotent_disable_datacenter(self, *args):
set_module_args(dict(
state='disabled',
password='admin',
server='localhost',
user='admin',
name='foo'
))
client = AnsibleF5Client(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
f5_product_name=self.spec.f5_product_name
)
current = Parameters(load_fixture('load_gtm_datacenter_disabled.json'))
mm = ModuleManager(client)
# Override methods to force specific logic in the module to happen
mm.exists = Mock(return_value=True)
mm.update_on_device = Mock(return_value=True)
mm.read_current_from_device = Mock(return_value=current)
results = mm.exec_module()
assert results['changed'] is False
assert results['enabled'] is False
@patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root',
return_value=True)
class TestLegacyManager(unittest.TestCase):
def setUp(self):
self.spec = ArgumentSpec()
def test_legacy_disable_datacenter(self, *args):
set_module_args(dict(
state='present',
enabled='no',
password='admin',
server='localhost',
user='admin',
name='foo'
))
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.exists = Mock(side_effect=[False, True])
mm.create_on_device = Mock(return_value=True)
results = mm.exec_module()
assert results['changed'] is True
assert results['enabled'] is False
def test_legacy_enable_datacenter(self, *args):
set_module_args(dict(
state='present',
enabled='yes',
password='admin',
server='localhost',
user='admin',
name='foo'
))
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.exists = Mock(side_effect=[False, True])
mm.create_on_device = Mock(return_value=True)
results = mm.exec_module()
assert results['changed'] is True
assert results['enabled'] is True