Adds bigip_vcmp_guest module (#33024)

This module can be used to manage guests on a vCMP provisioned BIG-IP.
vCMP is a hardware-only feature, therefore this module cannot be used
on the VE editions of BIG-IP.
This commit is contained in:
Tim Rupp 2017-11-17 12:11:52 -08:00 committed by GitHub
parent 67d5e1d3e7
commit c94d57311c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 899 additions and 0 deletions

View file

@ -0,0 +1,714 @@
#!/usr/bin/python
# -*- 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: bigip_vcmp_guest
short_description: Manages vCMP guests on a BIG-IP
description:
- Manages vCMP guests on a BIG-IP. This functionality only exists on
actual hardware and must be enabled by provisioning C(vcmp) with the
C(bigip_provision) module.
version_added: "2.5"
options:
name:
description:
- The name of the vCMP guest to manage.
required: True
vlans:
description:
- VLANs that the guest uses to communicate with other guests, the host, and with
the external network. The available VLANs in the list are those that are
currently configured on the vCMP host.
- The order of these VLANs is not important; in fact, it's ignored. This module will
order the VLANs for you automatically. Therefore, if you deliberately re-order them
in subsequent tasks, you will find that this module will B(not) register a change.
initial_image:
description:
- Specifies the base software release ISO image file for installing the TMOS
hypervisor instance and any licensed BIG-IP modules onto the guest's virtual
disk. When creating a new guest, this parameter is required.
mgmt_network:
description:
- Specifies the method by which the management address is used in the vCMP guest.
- When C(bridged), specifies that the guest can communicate with the vCMP host's
management network.
- When C(isolated), specifies that the guest is isolated from the vCMP host's
management network. In this case, the only way that a guest can communicate
with the vCMP host is through the console port or through a self IP address
on the guest that allows traffic through port 22.
- When C(host only), prevents the guest from installing images and hotfixes other
than those provided by the hypervisor.
- If the guest setting is C(isolated) or C(host only), the C(mgmt_address) does
not apply.
- Concerning mode changing, changing C(bridged) to C(isolated) causes the vCMP
host to remove all of the guest's management interfaces from its bridged
management network. This immediately disconnects the guest's VMs from the
physical management network. Changing C(isolated) to C(bridged) causes the
vCMP host to dynamically add the guest's management interfaces to the bridged
management network. This immediately connects all of the guest's VMs to the
physical management network. Changing this property while the guest is in the
C(configured) or C(provisioned) state has no immediate effect.
choices:
- bridged
- isolated
- host only
delete_virtual_disk:
description:
- When C(state) is C(absent), will additionally delete the virtual disk associated
with the vCMP guest. By default, this value is C(no).
default: no
mgmt_address:
description:
- Specifies the IP address, and subnet or subnet mask that you use to access
the guest when you want to manage a module running within the guest. This
parameter is required if the C(mgmt_network) parameter is C(bridged).
- When creating a new guest, if you do not specify a network or network mask,
a default of C(/24) (C(255.255.255.0)) will be assumed.
mgmt_route:
description:
- Specifies the gateway address for the C(mgmt_address).
- If this value is not specified when creating a new guest, it is set to C(none).
- The value C(none) can be used during an update to remove this value.
state:
description:
- The state of the vCMP guest on the system. Each state implies the actions of
all states before it.
- When C(configured), guarantees that the vCMP guest exists with the provided
attributes. Additionally, ensures that the vCMP guest is turned off.
- When C(disabled), behaves the same as C(configured) the name of this state
is just a convenience for the user that is more understandable.
- When C(provisioned), will ensure that the guest is created and installed.
This state will not start the guest; use C(deployed) for that. This state
is one step beyond C(present) as C(present) will not install the guest;
only setup the configuration for it to be installed.
- When C(present), ensures the guest is properly provisioned and starts
the guest so that it is in a running state.
- When C(absent), removes the vCMP from the system.
default: "present"
choices:
- configured
- disabled
- provisioned
- present
- absent
cores_per_slot:
description:
- Specifies the number of cores that the system allocates to the guest.
- Each core represents a portion of CPU and memory. Therefore, the amount of
memory allocated per core is directly tied to the amount of CPU. This amount
of memory varies per hardware platform type.
- The number you can specify depends on the type of hardware you have.
- In the event of a reboot, the system persists the guest to the same slot on
which it ran prior to the reboot.
notes:
- Requires the f5-sdk Python package on the host. This is as easy as pip
install f5-sdk.
- 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
device. This boot time is very similar in length to the time it takes to
boot VMs on any other virtualization platform; public or private.
- When BIG-IP starts, the VMs are booted sequentially; not in parallel. This
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
BIG-IP chassis will be available before all vCMP guests are online.
requirements:
- f5-sdk >= 3.0.3
- netaddr
extends_documentation_fragment: f5
author:
- Tim Rupp (@caphrim007)
'''
EXAMPLES = r'''
- name: Create a vCMP guest
bigip_vcmp_guest:
name: foo
password: secret
server: lb.mydomain.com
state: present
user: admin
mgmt_network: bridge
mgmt_address: 10.20.30.40/24
delegate_to: localhost
- name: Create a vCMP guest with specific VLANs
bigip_vcmp_guest:
name: foo
password: secret
server: lb.mydomain.com
state: present
user: admin
mgmt_network: bridge
mgmt_address: 10.20.30.40/24
vlans:
- vlan1
- vlan2
delegate_to: localhost
- name: Remove vCMP guest and disk
bigip_vcmp_guest:
name: guest1
state: absent
delete_virtual_disk: yes
register: result
'''
RETURN = r'''
vlans:
description: The VLANs assigned to the vCMP guest, in their full path format.
returned: changed
type: list
sample: ['/Common/vlan1', '/Common/vlan2']
'''
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
from ansible.module_utils.six import iteritems
from collections import defaultdict
from collections import namedtuple
import time
try:
from netaddr import IPAddress, AddrFormatError, IPNetwork
HAS_NETADDR = True
except ImportError:
HAS_NETADDR = False
try:
from f5.utils.responses.handlers import Stats
from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError
except ImportError:
HAS_F5SDK = False
class Parameters(AnsibleF5Parameters):
api_map = {
'managementGw': 'mgmt_route',
'managementNetwork': 'mgmt_network',
'managementIp': 'mgmt_address',
'initialImage': 'initial_image',
'virtualDisk': 'virtual_disk',
'coresPerSlot': 'cores_per_slot'
}
api_attributes = [
'vlans', 'managementNetwork', 'managementIp', 'initialImage', 'managementGw',
'state'
]
returnables = [
'vlans', 'mgmt_network', 'mgmt_address', 'initial_image', 'mgmt_route',
'name'
]
updatables = [
'vlans', 'mgmt_network', 'mgmt_address', 'initial_image', 'mgmt_route',
'state'
]
def __init__(self, params=None, client=None):
self._values = defaultdict(lambda: None)
self._values['__warnings'] = []
if params:
self.update(params=params)
self.client = client
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 _fqdn_name(self, value):
if value is not None and not value.startswith('/'):
return '/{0}/{1}'.format(self.partition, value)
return value
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
@property
def mgmt_route(self):
if self._values['mgmt_route'] is None:
return None
elif self._values['mgmt_route'] == 'none':
return 'none'
try:
result = IPAddress(self._values['mgmt_route'])
return str(result)
except AddrFormatError:
raise F5ModuleError(
"The specified 'mgmt_route' is not a valid IP address"
)
@property
def mgmt_address(self):
if self._values['mgmt_address'] is None:
return None
try:
addr = IPNetwork(self._values['mgmt_address'])
result = '{0}/{1}'.format(addr.ip, addr.prefixlen)
return result
except AddrFormatError:
raise F5ModuleError(
"The specified 'mgmt_address' is not a valid IP address"
)
@property
def mgmt_tuple(self):
result = None
Destination = namedtuple('Destination', ['ip', 'subnet'])
try:
parts = self._values['mgmt_address'].split('/')
if len(parts) == 2:
result = Destination(ip=parts[0], subnet=parts[1])
elif len(parts) < 2:
result = Destination(ip=parts[0], subnet=None)
else:
F5ModuleError(
"The provided mgmt_address is malformed."
)
except ValueError:
result = Destination(ip=None, subnet=None)
return result
@property
def state(self):
if self._values['state'] == 'present':
return 'deployed'
elif self._values['state'] in ['configured', 'disabled']:
return 'configured'
return self._values['state']
@property
def vlans(self):
if self._values['vlans'] is None:
return None
result = [self._fqdn_name(x) for x in self._values['vlans']]
result.sort()
return result
@property
def initial_image(self):
if self._values['initial_image'] is None:
return None
if self.initial_image_exists(self._values['initial_image']):
return self._values['initial_image']
raise F5ModuleError(
"The specified 'initial_image' does not exist on the remote device"
)
def initial_image_exists(self, image):
collection = self.client.api.tm.sys.software.images.get_collection()
for resource in collection:
if resource.name.startswith(image):
return True
return False
class Changes(Parameters):
pass
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:
return self.__default(param)
def __default(self, param):
attr1 = getattr(self.want, param)
try:
attr2 = getattr(self.have, param)
if attr1 != attr2:
return attr1
except AttributeError:
return attr1
@property
def mgmt_address(self):
want = self.want.mgmt_tuple
if want.subnet is None:
raise F5ModuleError(
"A subnet must be specified when changing the mgmt_address"
)
if self.want.mgmt_address != self.have.mgmt_address:
return self.want.mgmt_address
class ModuleManager(object):
def __init__(self, client):
self.client = client
self.want = Parameters(client=client, params=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):
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 should_update(self):
result = self._update_changed_options()
if result:
return True
return False
def exec_module(self):
changed = False
result = dict()
state = self.want.state
try:
if state in ['configured', 'provisioned', 'deployed']:
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))
self._announce_deprecations(result)
return result
def _announce_deprecations(self, result):
warnings = result.pop('__warnings', [])
for warning in warnings:
self.client.module.deprecate(
msg=warning['msg'],
version=warning['version']
)
def _fqdn_name(self, value):
if value is not None and not value.startswith('/'):
return '/{0}/{1}'.format(self.partition, value)
return value
def present(self):
if self.exists():
return self.update()
else:
return self.create()
def exists(self):
result = self.client.api.tm.vcmp.guests.guest.exists(
name=self.want.name
)
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()
if self.want.state == 'provisioned':
self.provision()
elif self.want.state == 'deployed':
self.deploy()
elif self.want.state == 'configured':
self.configure()
return True
def remove(self):
if self.client.check_mode:
return True
if self.want.delete_virtual_disk:
self.have = self.read_current_from_device()
self.remove_from_device()
if self.exists():
raise F5ModuleError("Failed to delete the resource.")
if self.want.delete_virtual_disk:
self.remove_virtual_disk()
return True
def create(self):
self._set_changed_options()
if self.client.check_mode:
return True
if self.want.mgmt_tuple.subnet is None:
self.want.update(dict(
mgmt_address='{0}/255.255.255.0'.format(self.want.mgmt_tuple.ip)
))
self.create_on_device()
if self.want.state == 'provisioned':
self.provision()
elif self.want.state == 'deployed':
self.deploy()
elif self.want.state == 'configured':
self.configure()
return True
def create_on_device(self):
params = self.want.api_params()
self.client.api.tm.vcmp.guests.guest.create(
name=self.want.name,
**params
)
def update_on_device(self):
params = self.changes.api_params()
resource = self.client.api.tm.vcmp.guests.guest.load(
name=self.want.name
)
resource.modify(**params)
def absent(self):
if self.exists():
return self.remove()
return False
def remove_from_device(self):
resource = self.client.api.tm.vcmp.guests.guest.load(
name=self.want.name
)
if resource:
resource.delete()
def read_current_from_device(self):
resource = self.client.api.tm.vcmp.guests.guest.load(
name=self.want.name
)
result = resource.attrs
return Parameters(result)
def remove_virtual_disk(self):
if self.virtual_disk_exists():
return self.remove_virtual_disk_from_device()
return False
def virtual_disk_exists(self):
collection = self.client.api.tm.vcmp.virtual_disks.get_collection()
for resource in collection:
check = '{0}/'.format(self.have.virtual_disk)
if resource.name.startswith(check):
return True
return False
def remove_virtual_disk_from_device(self):
collection = self.client.api.tm.vcmp.virtual_disks.get_collection()
for resource in collection:
check = '{0}/'.format(self.have.virtual_disk)
if resource.name.startswith(check):
resource.delete()
return True
return False
def is_configured(self):
"""Checks to see if guest is disabled
A disabled guest is fully disabled once their Stats go offline.
Until that point they are still in the process of disabling.
:return:
"""
try:
res = self.client.api.tm.vcmp.guests.guest.load(name=self.want.name)
Stats(res.stats.load())
return False
except iControlUnexpectedHTTPError as ex:
if 'Object not found - ' in str(ex):
return True
raise
def is_provisioned(self):
try:
res = self.client.api.tm.vcmp.guests.guest.load(name=self.want.name)
stats = Stats(res.stats.load())
if stats.stat['requestedState']['description'] == 'provisioned':
if stats.stat['vmStatus']['description'] == 'stopped':
return True
except iControlUnexpectedHTTPError:
pass
return False
def is_deployed(self):
try:
res = self.client.api.tm.vcmp.guests.guest.load(name=self.want.name)
stats = Stats(res.stats.load())
if stats.stat['requestedState']['description'] == 'deployed':
if stats.stat['vmStatus']['description'] == 'running':
return True
except iControlUnexpectedHTTPError:
pass
return False
def configure(self):
if self.is_configured():
return False
self.configure_on_device()
self.wait_for_configured()
return True
def configure_on_device(self):
resource = self.client.api.tm.vcmp.guests.guest.load(name=self.want.name)
resource.modify(state='configured')
def wait_for_configured(self):
nops = 0
while nops < 3:
if self.is_configured():
nops += 1
time.sleep(1)
def provision(self):
if self.is_provisioned():
return False
self.provision_on_device()
self.wait_for_provisioned()
def provision_on_device(self):
resource = self.client.api.tm.vcmp.guests.guest.load(name=self.want.name)
resource.modify(state='provisioned')
def wait_for_provisioned(self):
nops = 0
while nops < 3:
if self.is_provisioned():
nops += 1
time.sleep(1)
def deploy(self):
if self.is_deployed():
return False
self.deploy_on_device()
self.wait_for_deployed()
def deploy_on_device(self):
resource = self.client.api.tm.vcmp.guests.guest.load(name=self.want.name)
resource.modify(state='deployed')
def wait_for_deployed(self):
nops = 0
while nops < 3:
if self.is_deployed():
nops += 1
time.sleep(1)
class ArgumentSpec(object):
def __init__(self):
self.supports_check_mode = True
self.argument_spec = dict(
name=dict(required=True),
vlans=dict(type='list'),
mgmt_network=dict(choices=['bridged', 'isolated', 'host only']),
mgmt_address=dict(),
mgmt_route=dict(),
initial_image=dict(),
state=dict(
default='present',
choices=['configured', 'disabled', 'provisioned', 'absent', 'present']
),
delete_virtual_disk=dict(
type='bool', default='no'
),
cores_per_slot=dict(type='int')
)
self.f5_product_name = 'bigip'
self.required_if = [
['mgmt_network', 'bridged', ['mgmt_address']]
]
def main():
if not HAS_F5SDK:
raise F5ModuleError("The python f5-sdk module is required")
if not HAS_NETADDR:
raise F5ModuleError("The python netaddr module is required")
spec = ArgumentSpec()
client = AnsibleF5Client(
argument_spec=spec.argument_spec,
supports_check_mode=spec.supports_check_mode,
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,185 @@
# -*- 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 pytest
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
from ansible.module_utils.f5_utils import F5ModuleError
try:
from library.bigip_vcmp_guest import Parameters
from library.bigip_vcmp_guest import ModuleManager
from library.bigip_vcmp_guest import ArgumentSpec
from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError
except ImportError:
try:
from ansible.modules.network.f5.bigip_vcmp_guest import Parameters
from ansible.modules.network.f5.bigip_vcmp_guest import ModuleManager
from ansible.modules.network.f5.bigip_vcmp_guest 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(
initial_image='BIGIP-12.1.0.1.0.1447-HF1.iso',
mgmt_network='bridged',
mgmt_address='1.2.3.4/24',
vlans=[
'vlan1',
'vlan2'
]
)
p = Parameters(args)
assert p.initial_image == 'BIGIP-12.1.0.1.0.1447-HF1.iso'
assert p.mgmt_network == 'bridged'
def test_module_parameters_mgmt_bridged_without_subnet(self):
args = dict(
mgmt_network='bridged',
mgmt_address='1.2.3.4'
)
p = Parameters(args)
assert p.mgmt_network == 'bridged'
assert p.mgmt_address == '1.2.3.4/32'
def test_module_parameters_mgmt_address_cidr(self):
args = dict(
mgmt_network='bridged',
mgmt_address='1.2.3.4/24'
)
p = Parameters(args)
assert p.mgmt_network == 'bridged'
assert p.mgmt_address == '1.2.3.4/24'
def test_module_parameters_mgmt_address_subnet(self):
args = dict(
mgmt_network='bridged',
mgmt_address='1.2.3.4/255.255.255.0'
)
p = Parameters(args)
assert p.mgmt_network == 'bridged'
assert p.mgmt_address == '1.2.3.4/24'
def test_module_parameters_mgmt_route(self):
args = dict(
mgmt_route='1.2.3.4'
)
p = Parameters(args)
assert p.mgmt_route == '1.2.3.4'
def test_module_parameters_vcmp_software_image_facts(self):
# vCMP images may include a forward slash in their names. This is probably
# related to the slots on the system, but it is not a valid value to specify
# that slot when providing an initial image
args = dict(
initial_image='BIGIP-12.1.0.1.0.1447-HF1.iso/1',
)
p = Parameters(args)
assert p.initial_image == 'BIGIP-12.1.0.1.0.1447-HF1.iso/1'
def test_api_parameters(self):
args = dict(
initialImage="BIGIP-tmos-tier2-13.1.0.0.0.931.iso",
managementGw="2.2.2.2",
managementIp="1.1.1.1/24",
managementNetwork="bridged",
state="deployed",
vlans=[
"/Common/vlan1",
"/Common/vlan2"
]
)
p = Parameters(args)
assert p.initial_image == 'BIGIP-tmos-tier2-13.1.0.0.0.931.iso'
assert p.mgmt_route == '2.2.2.2'
assert p.mgmt_address == '1.1.1.1/24'
assert '/Common/vlan1' in p.vlans
assert '/Common/vlan2' in p.vlans
@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_vlan(self, *args):
set_module_args(dict(
name="guest1",
mgmt_network="bridged",
mgmt_address="10.10.10.10/24",
initial_image="BIGIP-13.1.0.0.0.931.iso",
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 to force specific logic in the module to happen
mm = ModuleManager(client)
mm.create_on_device = Mock(return_value=True)
mm.exists = Mock(return_value=False)
mm.is_deployed = Mock(side_effect=[False, True, True, True, True])
mm.deploy_on_device = Mock(return_value=True)
results = mm.exec_module()
assert results['changed'] is True
assert results['name'] == 'guest1'