Adds module which allows users to manage partitions on a BIG-IP (#30577)

pull/30704/head
Tim Rupp 7 years ago committed by John R Barker
parent f8230d961d
commit 0eb0d96286

@ -0,0 +1,392 @@
#!/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_partition
short_description: Manage BIG-IP partitions.
description:
- Manage BIG-IP partitions.
version_added: "2.5"
options:
description:
description:
- The description to attach to the Partition.
route_domain:
description:
- The default Route Domain to assign to the Partition. If no route domain
is specified, then the default route domain for the system (typically
zero) will be used only when creating a new partition.
state:
description:
- Whether the partition should exist or not.
default: present
choices:
- present
- absent
notes:
- Requires the f5-sdk Python package on the host. This is as easy as pip
install f5-sdk.
- Requires BIG-IP software version >= 12
requirements:
- f5-sdk >= 2.2.3
extends_documentation_fragment: f5
author:
- Tim Rupp (@caphrim007)
'''
EXAMPLES = '''
- name: Create partition "foo" using the default route domain
bigip_partition:
name: "foo"
password: "secret"
server: "lb.mydomain.com"
user: "admin"
delegate_to: localhost
- name: Create partition "bar" using a custom route domain
bigip_partition:
name: "bar"
route_domain: 3
password: "secret"
server: "lb.mydomain.com"
user: "admin"
delegate_to: localhost
- name: Change route domain of partition "foo"
bigip_partition:
name: "foo"
route_domain: 8
password: "secret"
server: "lb.mydomain.com"
user: "admin"
delegate_to: localhost
- name: Set a description for partition "foo"
bigip_partition:
name: "foo"
description: "Tenant CompanyA"
password: "secret"
server: "lb.mydomain.com"
user: "admin"
delegate_to: localhost
- name: Delete the "foo" partition
bigip_partition:
name: "foo"
password: "secret"
server: "lb.mydomain.com"
user: "admin"
state: "absent"
delegate_to: localhost
'''
RETURN = '''
route_domain:
description: Name of the route domain associated with the partition.
returned: changed and success
type: int
sample: 0
description:
description: The description of the partition.
returned: changed and success
type: string
sample: "Example partition"
'''
from ansible.module_utils.f5_utils import AnsibleF5Client
from ansible.module_utils.f5_utils import AnsibleF5Parameters
from ansible.module_utils.f5_utils import F5ModuleError
from ansible.module_utils.f5_utils import iteritems
from ansible.module_utils.f5_utils import defaultdict
try:
from ansible.module_utils.f5_utils import HAS_F5SDK
from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError
except ImportError:
HAS_F5SDK = False
class Parameters(AnsibleF5Parameters):
api_map = {
'defaultRouteDomain': 'route_domain',
}
api_attributes = [
'description', 'defaultRouteDomain'
]
returnables = [
'description', 'route_domain'
]
updatables = [
'description', 'route_domain'
]
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 to_return(self):
result = {}
try:
for returnable in self.returnables:
result[returnable] = getattr(self, returnable)
result = self._filter_params(result)
return result
except Exception:
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
@property
def partition(self):
# Cannot create a partition in a partition, so nullify this
return None
@property
def route_domain(self):
if self._values['route_domain'] is None:
return None
return int(self._values['route_domain'])
class Difference(object):
def __init__(self, want, have=None):
self.want = want
self.have = have
def compare(self, param):
try:
result = getattr(self, param)
return result
except AttributeError:
result = self.__default(param)
return result
def __default(self, param):
attr1 = getattr(self.want, param)
try:
attr2 = getattr(self.have, param)
if attr1 != attr2:
return attr1
except AttributeError:
return attr1
class ModuleManager(object):
def __init__(self, client):
self.client = client
self.have = None
self.want = Parameters(self.client.module.params)
self.changes = Parameters()
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 = Parameters(changed)
def _update_changed_options(self):
diff = Difference(self.want, self.have)
updatables = Parameters.updatables
changed = dict()
for k in updatables:
change = diff.compare(k)
if change is None:
continue
else:
changed[k] = change
if changed:
self.changes = Parameters(changed)
return True
return False
def exec_module(self):
changed = False
result = dict()
state = self.want.state
try:
if state == "present":
changed = self.present()
elif state == "absent":
changed = self.absent()
except iControlUnexpectedHTTPError as e:
raise F5ModuleError(str(e))
changes = self.changes.to_return()
result.update(**changes)
result.update(dict(changed=changed))
return result
def present(self):
if self.exists():
return self.update()
else:
return self.create()
def create(self):
if self.client.check_mode:
return True
self.create_on_device()
if not self.exists():
raise F5ModuleError("Failed to create the partition.")
return True
def should_update(self):
result = self._update_changed_options()
if result:
return True
return False
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 absent(self):
if self.exists():
return self.remove()
return False
def remove(self):
if self.client.check_mode:
return True
self.remove_from_device()
if self.exists():
raise F5ModuleError("Failed to delete the partition.")
return True
def read_current_from_device(self):
resource = self.client.api.tm.auth.partitions.partition.load(
name=self.want.name
)
result = resource.attrs
return Parameters(result)
def exists(self):
result = self.client.api.tm.auth.partitions.partition.exists(
name=self.want.name
)
return result
def update_on_device(self):
params = self.want.api_params()
result = self.client.api.tm.auth.partitions.partition.load(
name=self.want.name
)
result.modify(**params)
def create_on_device(self):
params = self.want.api_params()
self.client.api.tm.auth.partitions.partition.create(
name=self.want.name,
**params
)
def remove_from_device(self):
result = self.client.api.tm.auth.partitions.partition.load(
name=self.want.name
)
if result:
result.delete()
class ArgumentSpec(object):
def __init__(self):
self.supports_check_mode = True
self.argument_spec = dict(
name=dict(required=True),
description=dict(),
route_domain=dict(type='int'),
)
self.f5_product_name = 'bigip'
def main():
try:
spec = ArgumentSpec()
client = AnsibleF5Client(
argument_spec=spec.argument_spec,
supports_check_mode=spec.supports_check_mode,
f5_product_name=spec.f5_product_name
)
if not HAS_F5SDK:
raise F5ModuleError("The python f5-sdk module is required")
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()

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

@ -1,16 +0,0 @@
---
# 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

@ -1,13 +0,0 @@
---
- name: Create pool
bigip_pool:
lb_method: "round-robin"
name: "cs1.pool"
state: "present"
register: result
- name: Assert Create pool
assert:
that:
- result|changed

@ -1,21 +0,0 @@
---
- 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

@ -1,23 +0,0 @@
---
- 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

@ -1,48 +0,0 @@
---
- 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

@ -0,0 +1,9 @@
{
"kind": "tm:auth:partition:partitionstate",
"name": "foo",
"fullPath": "foo",
"generation": 212,
"selfLink": "https://localhost/mgmt/tm/auth/partition/foo?ver=13.0.0",
"defaultRouteDomain": 0,
"description": "my description"
}

@ -0,0 +1,220 @@
# -*- 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 Liccense 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_partition import Parameters
from library.bigip_partition import ModuleManager
from library.bigip_partition import ArgumentSpec
except ImportError:
try:
from ansible.modules.network.f5.bigip_partition import Parameters
from ansible.modules.network.f5.bigip_partition import ModuleManager
from ansible.modules.network.f5.bigip_partition import ArgumentSpec
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(
name='foo',
description='my description',
route_domain=0
)
p = Parameters(args)
assert p.name == 'foo'
assert p.description == 'my description'
assert p.route_domain == 0
def test_module_parameters_string_domain(self):
args = dict(
name='foo',
route_domain='0'
)
p = Parameters(args)
assert p.name == 'foo'
assert p.route_domain == 0
def test_api_parameters(self):
args = dict(
name='foo',
description='my description',
defaultRouteDomain=1
)
p = Parameters(args)
assert p.name == 'foo'
assert p.description == 'my description'
assert p.route_domain == 1
@patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root',
return_value=True)
class TestManagerEcho(unittest.TestCase):
def setUp(self):
self.spec = ArgumentSpec()
def test_create_partition(self, *args):
set_module_args(dict(
name='foo',
description='my description',
server='localhost',
password='password',
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
)
# Override methods in the specific type of manager
mm = ModuleManager(client)
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_partition_idempotent(self, *args):
set_module_args(dict(
name='foo',
description='my description',
server='localhost',
password='password',
user='admin'
))
current = Parameters(load_fixture('load_tm_auth_partition.json'))
client = AnsibleF5Client(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
f5_product_name=self.spec.f5_product_name
)
# Override methods in the specific type of manager
mm = ModuleManager(client)
mm.exists = Mock(return_value=True)
mm.read_current_from_device = Mock(return_value=current)
results = mm.exec_module()
assert results['changed'] is False
def test_update_description(self, *args):
set_module_args(dict(
name='foo',
description='another description',
server='localhost',
password='password',
user='admin'
))
current = Parameters(load_fixture('load_tm_auth_partition.json'))
client = AnsibleF5Client(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
f5_product_name=self.spec.f5_product_name
)
# Override methods in the specific type of manager
mm = ModuleManager(client)
mm.exists = Mock(return_value=True)
mm.read_current_from_device = Mock(return_value=current)
mm.update_on_device = Mock(return_value=True)
results = mm.exec_module()
assert results['changed'] is True
assert results['description'] == 'another description'
def test_update_route_domain(self, *args):
set_module_args(dict(
name='foo',
route_domain=1,
server='localhost',
password='password',
user='admin'
))
current = Parameters(load_fixture('load_tm_auth_partition.json'))
client = AnsibleF5Client(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode,
f5_product_name=self.spec.f5_product_name
)
# Override methods in the specific type of manager
mm = ModuleManager(client)
mm.exists = Mock(return_value=True)
mm.read_current_from_device = Mock(return_value=current)
mm.update_on_device = Mock(return_value=True)
results = mm.exec_module()
assert results['changed'] is True
assert results['route_domain'] == 1
Loading…
Cancel
Save