|
|
@ -117,6 +117,34 @@ options:
|
|
|
|
description:
|
|
|
|
description:
|
|
|
|
- Device partition to manage resources on.
|
|
|
|
- Device partition to manage resources on.
|
|
|
|
default: Common
|
|
|
|
default: Common
|
|
|
|
|
|
|
|
number_of_slots:
|
|
|
|
|
|
|
|
description:
|
|
|
|
|
|
|
|
- Specifies the number of slots for the system to use for creating the guest.
|
|
|
|
|
|
|
|
- This value dictates how many cores a guest is allocated from each slot that
|
|
|
|
|
|
|
|
it is assigned to.
|
|
|
|
|
|
|
|
- Possible values are dependent on the type of blades being used in this cluster.
|
|
|
|
|
|
|
|
- The default value depends on the type of blades being used in this cluster.
|
|
|
|
|
|
|
|
version_added: 2.7
|
|
|
|
|
|
|
|
min_number_of_slots:
|
|
|
|
|
|
|
|
description:
|
|
|
|
|
|
|
|
- Specifies the minimum number of slots that the guest must be assigned to in
|
|
|
|
|
|
|
|
order to deploy.
|
|
|
|
|
|
|
|
- This field dictates the number of slots that the guest must be assigned to.
|
|
|
|
|
|
|
|
- If at the end of any allocation attempt the guest is not assigned to at least
|
|
|
|
|
|
|
|
this many slots, the attempt fails and the change that initiated it is reverted.
|
|
|
|
|
|
|
|
- A guest's C(min_number_of_slots) value cannot be greater than its C(number_of_slots).
|
|
|
|
|
|
|
|
version_added: 2.7
|
|
|
|
|
|
|
|
allowed_slots:
|
|
|
|
|
|
|
|
description:
|
|
|
|
|
|
|
|
- Contains those slots that the guest is allowed to be assigned to.
|
|
|
|
|
|
|
|
- When the host determines which slots this guest should be assigned to, only slots
|
|
|
|
|
|
|
|
in this list will be considered.
|
|
|
|
|
|
|
|
- This is a good way to force guests to be assigned only to particular slots, or,
|
|
|
|
|
|
|
|
by configuring disjoint C(allowed_slots) on two guests, that those guests are
|
|
|
|
|
|
|
|
never assigned to the same slot.
|
|
|
|
|
|
|
|
- By default this list includes every available slot in the cluster. This means,
|
|
|
|
|
|
|
|
by default, the guest may be assigned to any slot.
|
|
|
|
|
|
|
|
version_added: 2.7
|
|
|
|
notes:
|
|
|
|
notes:
|
|
|
|
- This module can take a lot of time to deploy vCMP guests. This is an intrinsic
|
|
|
|
- This module can take a lot of time to deploy vCMP guests. This is an intrinsic
|
|
|
|
limitation of the vCMP system because it is booting real VMs on the BIG-IP
|
|
|
|
limitation of the vCMP system because it is booting real VMs on the BIG-IP
|
|
|
@ -126,7 +154,6 @@ notes:
|
|
|
|
means that it is not unusual for a vCMP host with many guests to take a
|
|
|
|
means that it is not unusual for a vCMP host with many guests to take a
|
|
|
|
long time (60+ minutes) to reboot and bring all the guests online. The
|
|
|
|
long time (60+ minutes) to reboot and bring all the guests online. The
|
|
|
|
BIG-IP chassis will be available before all vCMP guests are online.
|
|
|
|
BIG-IP chassis will be available before all vCMP guests are online.
|
|
|
|
- netaddr
|
|
|
|
|
|
|
|
extends_documentation_fragment: f5
|
|
|
|
extends_documentation_fragment: f5
|
|
|
|
author:
|
|
|
|
author:
|
|
|
|
- Tim Rupp (@caphrim007)
|
|
|
|
- Tim Rupp (@caphrim007)
|
|
|
@ -188,6 +215,8 @@ try:
|
|
|
|
from library.module_utils.network.f5.common import cleanup_tokens
|
|
|
|
from library.module_utils.network.f5.common import cleanup_tokens
|
|
|
|
from library.module_utils.network.f5.common import fq_name
|
|
|
|
from library.module_utils.network.f5.common import fq_name
|
|
|
|
from library.module_utils.network.f5.common import f5_argument_spec
|
|
|
|
from library.module_utils.network.f5.common import f5_argument_spec
|
|
|
|
|
|
|
|
from library.module_utils.network.f5.ipaddress import is_valid_ip
|
|
|
|
|
|
|
|
from library.module_utils.compat.ipaddress import ip_interface
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
|
|
|
|
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
|
|
|
|
from f5.utils.responses.handlers import Stats
|
|
|
|
from f5.utils.responses.handlers import Stats
|
|
|
@ -201,18 +230,14 @@ except ImportError:
|
|
|
|
from ansible.module_utils.network.f5.common import cleanup_tokens
|
|
|
|
from ansible.module_utils.network.f5.common import cleanup_tokens
|
|
|
|
from ansible.module_utils.network.f5.common import fq_name
|
|
|
|
from ansible.module_utils.network.f5.common import fq_name
|
|
|
|
from ansible.module_utils.network.f5.common import f5_argument_spec
|
|
|
|
from ansible.module_utils.network.f5.common import f5_argument_spec
|
|
|
|
|
|
|
|
from ansible.module_utils.network.f5.ipaddress import is_valid_ip
|
|
|
|
|
|
|
|
from ansible.module_utils.compat.ipaddress import ip_interface
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
|
|
|
|
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
|
|
|
|
from f5.utils.responses.handlers import Stats
|
|
|
|
from f5.utils.responses.handlers import Stats
|
|
|
|
except ImportError:
|
|
|
|
except ImportError:
|
|
|
|
HAS_F5SDK = False
|
|
|
|
HAS_F5SDK = False
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
from netaddr import IPAddress, AddrFormatError, IPNetwork
|
|
|
|
|
|
|
|
HAS_NETADDR = True
|
|
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
|
|
HAS_NETADDR = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Parameters(AnsibleF5Parameters):
|
|
|
|
class Parameters(AnsibleF5Parameters):
|
|
|
|
api_map = {
|
|
|
|
api_map = {
|
|
|
@ -221,22 +246,49 @@ class Parameters(AnsibleF5Parameters):
|
|
|
|
'managementIp': 'mgmt_address',
|
|
|
|
'managementIp': 'mgmt_address',
|
|
|
|
'initialImage': 'initial_image',
|
|
|
|
'initialImage': 'initial_image',
|
|
|
|
'virtualDisk': 'virtual_disk',
|
|
|
|
'virtualDisk': 'virtual_disk',
|
|
|
|
'coresPerSlot': 'cores_per_slot'
|
|
|
|
'coresPerSlot': 'cores_per_slot',
|
|
|
|
|
|
|
|
'slots': 'number_of_slots',
|
|
|
|
|
|
|
|
'minSlots': 'min_number_of_slots',
|
|
|
|
|
|
|
|
'allowedSlots': 'allowed_slots',
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
api_attributes = [
|
|
|
|
api_attributes = [
|
|
|
|
'vlans', 'managementNetwork', 'managementIp', 'initialImage', 'managementGw',
|
|
|
|
'vlans',
|
|
|
|
'state'
|
|
|
|
'managementNetwork',
|
|
|
|
|
|
|
|
'managementIp',
|
|
|
|
|
|
|
|
'initialImage',
|
|
|
|
|
|
|
|
'managementGw',
|
|
|
|
|
|
|
|
'state',
|
|
|
|
|
|
|
|
'coresPerSlot',
|
|
|
|
|
|
|
|
'slots',
|
|
|
|
|
|
|
|
'minSlots',
|
|
|
|
|
|
|
|
'allowedSlots',
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
returnables = [
|
|
|
|
returnables = [
|
|
|
|
'vlans', 'mgmt_network', 'mgmt_address', 'initial_image', 'mgmt_route',
|
|
|
|
'vlans',
|
|
|
|
'name'
|
|
|
|
'mgmt_network',
|
|
|
|
|
|
|
|
'mgmt_address',
|
|
|
|
|
|
|
|
'initial_image',
|
|
|
|
|
|
|
|
'mgmt_route',
|
|
|
|
|
|
|
|
'name',
|
|
|
|
|
|
|
|
'cores_per_slot',
|
|
|
|
|
|
|
|
'number_of_slots',
|
|
|
|
|
|
|
|
'min_number_of_slots',
|
|
|
|
|
|
|
|
'allowed_slots',
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
updatables = [
|
|
|
|
updatables = [
|
|
|
|
'vlans', 'mgmt_network', 'mgmt_address', 'initial_image', 'mgmt_route',
|
|
|
|
'vlans',
|
|
|
|
'state'
|
|
|
|
'mgmt_network',
|
|
|
|
|
|
|
|
'mgmt_address',
|
|
|
|
|
|
|
|
'initial_image',
|
|
|
|
|
|
|
|
'mgmt_route',
|
|
|
|
|
|
|
|
'state',
|
|
|
|
|
|
|
|
'cores_per_slot',
|
|
|
|
|
|
|
|
'number_of_slots',
|
|
|
|
|
|
|
|
'min_number_of_slots',
|
|
|
|
|
|
|
|
'allowed_slots',
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def to_return(self):
|
|
|
|
def to_return(self):
|
|
|
@ -255,10 +307,9 @@ class Parameters(AnsibleF5Parameters):
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
elif self._values['mgmt_route'] == 'none':
|
|
|
|
elif self._values['mgmt_route'] == 'none':
|
|
|
|
return 'none'
|
|
|
|
return 'none'
|
|
|
|
try:
|
|
|
|
if is_valid_ip(self._values['mgmt_route']):
|
|
|
|
result = IPAddress(self._values['mgmt_route'])
|
|
|
|
return self._values['mgmt_route']
|
|
|
|
return str(result)
|
|
|
|
else:
|
|
|
|
except AddrFormatError:
|
|
|
|
|
|
|
|
raise F5ModuleError(
|
|
|
|
raise F5ModuleError(
|
|
|
|
"The specified 'mgmt_route' is not a valid IP address"
|
|
|
|
"The specified 'mgmt_route' is not a valid IP address"
|
|
|
|
)
|
|
|
|
)
|
|
|
@ -268,10 +319,9 @@ class Parameters(AnsibleF5Parameters):
|
|
|
|
if self._values['mgmt_address'] is None:
|
|
|
|
if self._values['mgmt_address'] is None:
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
addr = IPNetwork(self._values['mgmt_address'])
|
|
|
|
addr = ip_interface(u'%s' % str(self._values['mgmt_address']))
|
|
|
|
result = '{0}/{1}'.format(addr.ip, addr.prefixlen)
|
|
|
|
return str(addr.with_prefixlen)
|
|
|
|
return result
|
|
|
|
except ValueError:
|
|
|
|
except AddrFormatError:
|
|
|
|
|
|
|
|
raise F5ModuleError(
|
|
|
|
raise F5ModuleError(
|
|
|
|
"The specified 'mgmt_address' is not a valid IP address"
|
|
|
|
"The specified 'mgmt_address' is not a valid IP address"
|
|
|
|
)
|
|
|
|
)
|
|
|
@ -327,11 +377,35 @@ class Parameters(AnsibleF5Parameters):
|
|
|
|
return True
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
|
|
def allowed_slots(self):
|
|
|
|
|
|
|
|
if self._values['allowed_slots'] is None:
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
result = self._values['allowed_slots']
|
|
|
|
|
|
|
|
result.sort()
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ApiParameters(Parameters):
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ModuleParameters(Parameters):
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Changes(Parameters):
|
|
|
|
class Changes(Parameters):
|
|
|
|
pass
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UsableChanges(Parameters):
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ReportableChanges(Parameters):
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Difference(object):
|
|
|
|
class Difference(object):
|
|
|
|
def __init__(self, want, have=None):
|
|
|
|
def __init__(self, want, have=None):
|
|
|
|
self.want = want
|
|
|
|
self.want = want
|
|
|
@ -363,12 +437,21 @@ class Difference(object):
|
|
|
|
if self.want.mgmt_address != self.have.mgmt_address:
|
|
|
|
if self.want.mgmt_address != self.have.mgmt_address:
|
|
|
|
return self.want.mgmt_address
|
|
|
|
return self.want.mgmt_address
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
|
|
def allowed_slots(self):
|
|
|
|
|
|
|
|
if self.want.allowed_slots is None:
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
if self.have.allowed_slots is None:
|
|
|
|
|
|
|
|
return self.want.allowed_slots
|
|
|
|
|
|
|
|
if set(self.want.allowed_slots) != set(self.have.allowed_slots):
|
|
|
|
|
|
|
|
return self.want.allowed_slots
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ModuleManager(object):
|
|
|
|
class ModuleManager(object):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
self.module = kwargs.get('module', None)
|
|
|
|
self.module = kwargs.get('module', None)
|
|
|
|
self.client = kwargs.get('client', None)
|
|
|
|
self.client = kwargs.get('client', None)
|
|
|
|
self.want = Parameters(client=self.client, params=self.module.params)
|
|
|
|
self.want = ModuleParameters(client=self.client, params=self.module.params)
|
|
|
|
self.changes = Changes()
|
|
|
|
self.changes = Changes()
|
|
|
|
|
|
|
|
|
|
|
|
def _set_changed_options(self):
|
|
|
|
def _set_changed_options(self):
|
|
|
@ -377,7 +460,7 @@ class ModuleManager(object):
|
|
|
|
if getattr(self.want, key) is not None:
|
|
|
|
if getattr(self.want, key) is not None:
|
|
|
|
changed[key] = getattr(self.want, key)
|
|
|
|
changed[key] = getattr(self.want, key)
|
|
|
|
if changed:
|
|
|
|
if changed:
|
|
|
|
self.changes = Changes(params=changed)
|
|
|
|
self.changes = UsableChanges(params=changed)
|
|
|
|
|
|
|
|
|
|
|
|
def _update_changed_options(self):
|
|
|
|
def _update_changed_options(self):
|
|
|
|
diff = Difference(self.want, self.have)
|
|
|
|
diff = Difference(self.want, self.have)
|
|
|
@ -390,7 +473,7 @@ class ModuleManager(object):
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
changed[k] = change
|
|
|
|
changed[k] = change
|
|
|
|
if changed:
|
|
|
|
if changed:
|
|
|
|
self.changes = Parameters(params=changed)
|
|
|
|
self.changes = UsableChanges(params=changed)
|
|
|
|
return True
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
@ -445,6 +528,9 @@ class ModuleManager(object):
|
|
|
|
return False
|
|
|
|
return False
|
|
|
|
if self.module.check_mode:
|
|
|
|
if self.module.check_mode:
|
|
|
|
return True
|
|
|
|
return True
|
|
|
|
|
|
|
|
if self.changes.cores_per_slot:
|
|
|
|
|
|
|
|
if not self.is_configured():
|
|
|
|
|
|
|
|
self.configure()
|
|
|
|
self.update_on_device()
|
|
|
|
self.update_on_device()
|
|
|
|
if self.want.state == 'provisioned':
|
|
|
|
if self.want.state == 'provisioned':
|
|
|
|
self.provision()
|
|
|
|
self.provision()
|
|
|
@ -514,7 +600,7 @@ class ModuleManager(object):
|
|
|
|
name=self.want.name
|
|
|
|
name=self.want.name
|
|
|
|
)
|
|
|
|
)
|
|
|
|
result = resource.attrs
|
|
|
|
result = resource.attrs
|
|
|
|
return Parameters(params=result)
|
|
|
|
return ApiParameters(params=result)
|
|
|
|
|
|
|
|
|
|
|
|
def remove_virtual_disk(self):
|
|
|
|
def remove_virtual_disk(self):
|
|
|
|
if self.virtual_disk_exists():
|
|
|
|
if self.virtual_disk_exists():
|
|
|
@ -522,9 +608,27 @@ class ModuleManager(object):
|
|
|
|
return False
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def virtual_disk_exists(self):
|
|
|
|
def virtual_disk_exists(self):
|
|
|
|
|
|
|
|
"""Checks if a virtual disk exists for a guest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The virtual disk names can differ based on the device vCMP is installed on.
|
|
|
|
|
|
|
|
For instance, on a shuttle-series device with no slots, you will see disks
|
|
|
|
|
|
|
|
that resemble the following
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
guest1.img
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
On an 8-blade Viprion with slots though, you will see
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
guest1.img/1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The "/1" in this case is the slot that it is a part of. This method looks
|
|
|
|
|
|
|
|
for the virtual-disk without the trailing slot.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
|
|
bool: True on success. False otherwise.
|
|
|
|
|
|
|
|
"""
|
|
|
|
collection = self.client.api.tm.vcmp.virtual_disks.get_collection()
|
|
|
|
collection = self.client.api.tm.vcmp.virtual_disks.get_collection()
|
|
|
|
for resource in collection:
|
|
|
|
for resource in collection:
|
|
|
|
check = '{0}/'.format(self.have.virtual_disk)
|
|
|
|
check = '{0}'.format(self.have.virtual_disk)
|
|
|
|
if resource.name.startswith(check):
|
|
|
|
if resource.name.startswith(check):
|
|
|
|
return True
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
return False
|
|
|
@ -532,7 +636,7 @@ class ModuleManager(object):
|
|
|
|
def remove_virtual_disk_from_device(self):
|
|
|
|
def remove_virtual_disk_from_device(self):
|
|
|
|
collection = self.client.api.tm.vcmp.virtual_disks.get_collection()
|
|
|
|
collection = self.client.api.tm.vcmp.virtual_disks.get_collection()
|
|
|
|
for resource in collection:
|
|
|
|
for resource in collection:
|
|
|
|
check = '{0}/'.format(self.have.virtual_disk)
|
|
|
|
check = '{0}'.format(self.have.virtual_disk)
|
|
|
|
if resource.name.startswith(check):
|
|
|
|
if resource.name.startswith(check):
|
|
|
|
resource.delete()
|
|
|
|
resource.delete()
|
|
|
|
return True
|
|
|
|
return True
|
|
|
@ -645,9 +749,13 @@ class ArgumentSpec(object):
|
|
|
|
choices=['configured', 'disabled', 'provisioned', 'absent', 'present']
|
|
|
|
choices=['configured', 'disabled', 'provisioned', 'absent', 'present']
|
|
|
|
),
|
|
|
|
),
|
|
|
|
delete_virtual_disk=dict(
|
|
|
|
delete_virtual_disk=dict(
|
|
|
|
type='bool', default='no'
|
|
|
|
type='bool',
|
|
|
|
|
|
|
|
default='no'
|
|
|
|
),
|
|
|
|
),
|
|
|
|
cores_per_slot=dict(type='int'),
|
|
|
|
cores_per_slot=dict(type='int'),
|
|
|
|
|
|
|
|
number_of_slots=dict(type='int'),
|
|
|
|
|
|
|
|
min_number_of_slots=dict(type='int'),
|
|
|
|
|
|
|
|
allowed_slots=dict(type='list'),
|
|
|
|
partition=dict(
|
|
|
|
partition=dict(
|
|
|
|
default='Common',
|
|
|
|
default='Common',
|
|
|
|
fallback=(env_fallback, ['F5_PARTITION'])
|
|
|
|
fallback=(env_fallback, ['F5_PARTITION'])
|
|
|
@ -670,8 +778,6 @@ def main():
|
|
|
|
)
|
|
|
|
)
|
|
|
|
if not HAS_F5SDK:
|
|
|
|
if not HAS_F5SDK:
|
|
|
|
module.fail_json(msg="The python f5-sdk module is required")
|
|
|
|
module.fail_json(msg="The python f5-sdk module is required")
|
|
|
|
if not HAS_NETADDR:
|
|
|
|
|
|
|
|
module.fail_json(msg="The python netaddr module is required")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
client = F5Client(**module.params)
|
|
|
|
client = F5Client(**module.params)
|
|
|
|