mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-26 20:31:27 -07:00
The wait parameter allows the module to wait or not wait when an application is created. By default the modules will wait.
747 lines
23 KiB
Python
747 lines
23 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright: (c) 2018, 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: bigiq_application_http
|
|
short_description: Manages BIG-IQ HTTP applications
|
|
description:
|
|
- Manages BIG-IQ applications used for load balancing an HTTP application on
|
|
port 80 on BIG-IP.
|
|
version_added: 2.6
|
|
options:
|
|
name:
|
|
description:
|
|
- Name of the new application.
|
|
required: True
|
|
description:
|
|
description:
|
|
- Description of the application.
|
|
servers:
|
|
description:
|
|
- A list of servers that the application is hosted on.
|
|
- If you are familiar with other BIG-IP setting, you might also refer to this
|
|
list as the list of pool members.
|
|
- When creating a new application, at least one server is required.
|
|
suboptions:
|
|
address:
|
|
description:
|
|
- The IP address of the server.
|
|
required: True
|
|
port:
|
|
description:
|
|
- The port of the server.
|
|
- When creating a new application and specifying a server, if this parameter
|
|
is not provided, the default of C(80) will be used.
|
|
default: 80
|
|
inbound_virtual:
|
|
description:
|
|
- Settings to configure the virtual which will receive the inbound connection.
|
|
- This virtual will be used to host the HTTP endpoint of the application.
|
|
suboptions:
|
|
address:
|
|
description:
|
|
- Specifies destination IP address information to which the virtual server
|
|
sends traffic.
|
|
- This parameter is required when creating a new application.
|
|
required: True
|
|
netmask:
|
|
description:
|
|
- Specifies the netmask to associate with the given C(destination).
|
|
- This parameter is required when creating a new application.
|
|
required: True
|
|
port:
|
|
description:
|
|
- The port that the virtual listens for connections on.
|
|
- When creating a new application, if this parameter is not specified, the
|
|
default value of C(80) will be used.
|
|
default: 80
|
|
service_environment:
|
|
description:
|
|
- Specifies the name of service environment that the application will be
|
|
deployed to.
|
|
- When creating a new application, this parameter is required.
|
|
- The service environment type will be discovered by this module automatically.
|
|
Therefore, it is crucial that you maintain unique names for items in the
|
|
different service environment types (at this time, SSGs and BIGIPs).
|
|
add_analytics:
|
|
description:
|
|
- Collects statistics of the BIG-IP that the application is deployed to.
|
|
- This parameter is only relevant when specifying a C(service_environment) which
|
|
is a BIG-IP; not an SSG.
|
|
type: bool
|
|
default: no
|
|
state:
|
|
description:
|
|
- The state of the resource on the system.
|
|
- When C(present), guarantees that the resource exists with the provided attributes.
|
|
- When C(absent), removes the resource from the system.
|
|
default: present
|
|
choices:
|
|
- absent
|
|
- present
|
|
wait:
|
|
description:
|
|
- If the module should wait for the application to be created, deleted or updated.
|
|
type: bool
|
|
default: yes
|
|
extends_documentation_fragment: f5
|
|
notes:
|
|
- This module does not support updating of your application (whether deployed or not).
|
|
If you need to update the application, the recommended practice is to remove and
|
|
re-create.
|
|
- Requires BIG-IQ version 6.0 or greater.
|
|
author:
|
|
- Tim Rupp (@caphrim007)
|
|
'''
|
|
|
|
EXAMPLES = r'''
|
|
- name: Load balance an HTTP application on port 80 on BIG-IP
|
|
bigiq_application_http:
|
|
name: my-app
|
|
description: Redirect HTTP to HTTPS
|
|
service_environment: my-ssg
|
|
servers:
|
|
- address: 1.2.3.4
|
|
port: 8080
|
|
- address: 5.6.7.8
|
|
port: 8080
|
|
inbound_virtual:
|
|
name: foo
|
|
destination: 2.2.2.2
|
|
netmask: 255.255.255.255
|
|
port: 443
|
|
provider:
|
|
password: secret
|
|
server: lb.mydomain.com
|
|
user: admin
|
|
state: present
|
|
delegate_to: localhost
|
|
'''
|
|
|
|
RETURN = r'''
|
|
description:
|
|
description: The new description of the application of the resource.
|
|
returned: changed
|
|
type: string
|
|
sample: My application
|
|
service_environment:
|
|
description: The environment which the service was deployed to.
|
|
returned: changed
|
|
type: string
|
|
sample: my-ssg1
|
|
inbound_virtual_destination:
|
|
description: The destination of the virtual that was created.
|
|
returned: changed
|
|
type: string
|
|
sample: 6.7.8.9
|
|
inbound_virtual_netmask:
|
|
description: The network mask of the provided inbound destination.
|
|
returned: changed
|
|
type: string
|
|
sample: 255.255.255.0
|
|
inbound_virtual_port:
|
|
description: The port the inbound virtual address listens on.
|
|
returned: changed
|
|
type: int
|
|
sample: 80
|
|
servers:
|
|
description: List of servers, and their ports, that make up the application.
|
|
type: complex
|
|
returned: changed
|
|
contains:
|
|
address:
|
|
description: The IP address of the server.
|
|
returned: changed
|
|
type: string
|
|
sample: 2.3.4.5
|
|
port:
|
|
description: The port that the server listens on.
|
|
returned: changed
|
|
type: int
|
|
sample: 8080
|
|
sample: hash/dictionary of values
|
|
'''
|
|
|
|
import time
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
from ansible.module_utils.basic import env_fallback
|
|
|
|
try:
|
|
from library.module_utils.network.f5.bigiq import F5RestClient
|
|
from library.module_utils.network.f5.common import F5ModuleError
|
|
from library.module_utils.network.f5.common import AnsibleF5Parameters
|
|
from library.module_utils.network.f5.common import f5_argument_spec
|
|
from library.module_utils.network.f5.common import exit_json
|
|
from library.module_utils.network.f5.common import fail_json
|
|
except ImportError:
|
|
from ansible.module_utils.network.f5.bigiq import F5RestClient
|
|
from ansible.module_utils.network.f5.common import F5ModuleError
|
|
from ansible.module_utils.network.f5.common import AnsibleF5Parameters
|
|
from ansible.module_utils.network.f5.common import f5_argument_spec
|
|
from ansible.module_utils.network.f5.common import exit_json
|
|
from ansible.module_utils.network.f5.common import fail_json
|
|
|
|
try:
|
|
import netaddr
|
|
HAS_NETADDR = True
|
|
except ImportError:
|
|
HAS_NETADDR = False
|
|
|
|
|
|
class Parameters(AnsibleF5Parameters):
|
|
api_map = {
|
|
'templateReference': 'template_reference',
|
|
'subPath': 'sub_path',
|
|
'ssgReference': 'ssg_reference',
|
|
'configSetName': 'config_set_name',
|
|
'defaultDeviceReference': 'default_device_reference',
|
|
'addAnalytics': 'add_analytics'
|
|
}
|
|
|
|
api_attributes = [
|
|
'resources', 'description', 'configSetName', 'subPath', 'templateReference',
|
|
'ssgReference', 'defaultDeviceReference', 'addAnalytics'
|
|
]
|
|
|
|
returnables = [
|
|
'resources', 'description', 'config_set_name', 'sub_path', 'template_reference',
|
|
'ssg_reference', 'default_device_reference', 'servers', 'inbound_virtual',
|
|
'add_analytics'
|
|
]
|
|
|
|
updatables = [
|
|
'resources', 'description', 'config_set_name', 'sub_path', 'template_reference',
|
|
'ssg_reference', 'default_device_reference', 'servers', 'add_analytics'
|
|
]
|
|
|
|
|
|
class ApiParameters(Parameters):
|
|
pass
|
|
|
|
|
|
class ModuleParameters(Parameters):
|
|
@property
|
|
def http_profile(self):
|
|
return "profile_http"
|
|
|
|
@property
|
|
def config_set_name(self):
|
|
return self.name
|
|
|
|
@property
|
|
def sub_path(self):
|
|
return self.name
|
|
|
|
@property
|
|
def template_reference(self):
|
|
filter = "name+eq+'Default-f5-HTTP-lb-template'"
|
|
uri = "https://{0}:{1}/mgmt/cm/global/templates/?$filter={2}&$top=1&$select=selfLink".format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port'],
|
|
filter
|
|
)
|
|
resp = self.client.api.get(uri)
|
|
try:
|
|
response = resp.json()
|
|
except ValueError as ex:
|
|
raise F5ModuleError(str(ex))
|
|
if resp.status == 200 and response['totalItems'] == 0:
|
|
raise F5ModuleError(
|
|
"No default HTTP LB template was found."
|
|
)
|
|
elif 'code' in response and response['code'] == 400:
|
|
if 'message' in response:
|
|
raise F5ModuleError(response['message'])
|
|
else:
|
|
raise F5ModuleError(resp._content)
|
|
|
|
result = dict(
|
|
link=response['items'][0]['selfLink']
|
|
)
|
|
return result
|
|
|
|
@property
|
|
def default_device_reference(self):
|
|
try:
|
|
# An IP address was specified
|
|
netaddr.IPAddress(self.service_environment)
|
|
filter = "address+eq+'{0}'".format(self.service_environment)
|
|
except netaddr.core.AddrFormatError:
|
|
# Assume a hostname was specified
|
|
filter = "hostname+eq+'{0}'".format(self.service_environment)
|
|
|
|
uri = "https://{0}:{1}/mgmt/shared/resolver/device-groups/cm-adccore-allbigipDevices/devices/?$filter={2}&$top=1&$select=selfLink".format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port'],
|
|
filter
|
|
)
|
|
resp = self.client.api.get(uri)
|
|
try:
|
|
response = resp.json()
|
|
except ValueError as ex:
|
|
raise F5ModuleError(str(ex))
|
|
if resp.status == 200 and response['totalItems'] == 0:
|
|
return None
|
|
elif 'code' in response and response['code'] == 400:
|
|
if 'message' in response:
|
|
raise F5ModuleError(response['message'])
|
|
else:
|
|
raise F5ModuleError(resp._content)
|
|
result = dict(
|
|
link=response['items'][0]['selfLink']
|
|
)
|
|
return result
|
|
|
|
@property
|
|
def ssg_reference(self):
|
|
filter = "name+eq+'{0}'".format(self.service_environment)
|
|
uri = "https://{0}:{1}/mgmt/cm/cloud/service-scaling-groups/?$filter={2}&$top=1&$select=selfLink".format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port'],
|
|
filter
|
|
)
|
|
resp = self.client.api.get(uri)
|
|
try:
|
|
response = resp.json()
|
|
except ValueError as ex:
|
|
raise F5ModuleError(str(ex))
|
|
if resp.status == 200 and response['totalItems'] == 0:
|
|
return None
|
|
elif 'code' in response and response['code'] == 400:
|
|
if 'message' in response:
|
|
raise F5ModuleError(response['message'])
|
|
else:
|
|
raise F5ModuleError(resp._content)
|
|
result = dict(
|
|
link=response['items'][0]['selfLink']
|
|
)
|
|
return result
|
|
|
|
|
|
class Changes(Parameters):
|
|
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
|
|
|
|
|
|
class UsableChanges(Changes):
|
|
@property
|
|
def resources(self):
|
|
result = dict()
|
|
result.update(self.http_profile)
|
|
result.update(self.http_monitor)
|
|
result.update(self.virtual)
|
|
result.update(self.pool)
|
|
result.update(self.nodes)
|
|
return result
|
|
|
|
@property
|
|
def virtual(self):
|
|
result = dict()
|
|
result['ltm:virtual::b487671f29ba'] = [
|
|
dict(
|
|
parameters=dict(
|
|
name='virtual',
|
|
destinationAddress=self.inbound_virtual['address'],
|
|
mask=self.inbound_virtual['netmask'],
|
|
destinationPort=self.inbound_virtual['port']
|
|
),
|
|
subcollectionResources=self.profiles
|
|
)
|
|
]
|
|
return result
|
|
|
|
@property
|
|
def profiles(self):
|
|
result = {
|
|
'profiles:9448fe71611e': [
|
|
dict(
|
|
parameters=dict()
|
|
)
|
|
],
|
|
'profiles:03a4950ab656': [
|
|
dict(
|
|
parameters=dict()
|
|
)
|
|
]
|
|
}
|
|
return result
|
|
|
|
@property
|
|
def pool(self):
|
|
result = dict()
|
|
result['ltm:pool:9a593d17495b'] = [
|
|
dict(
|
|
parameters=dict(
|
|
name='pool_0'
|
|
),
|
|
subcollectionResources=self.pool_members
|
|
)
|
|
]
|
|
return result
|
|
|
|
@property
|
|
def pool_members(self):
|
|
result = dict()
|
|
result['members:5109c66dfbac'] = []
|
|
for x in self.servers:
|
|
member = dict(
|
|
parameters=dict(
|
|
port=x['port'],
|
|
nodeReference=dict(
|
|
link='#/resources/ltm:node:9e76a6323321/{0}'.format(x['address']),
|
|
fullPath='# {0}'.format(x['address'])
|
|
)
|
|
)
|
|
)
|
|
result['members:5109c66dfbac'].append(member)
|
|
return result
|
|
|
|
@property
|
|
def http_profile(self):
|
|
result = dict()
|
|
result['ltm:profile:http:03a4950ab656'] = [
|
|
dict(
|
|
parameters=dict(
|
|
name='profile_http'
|
|
)
|
|
)
|
|
]
|
|
return result
|
|
|
|
@property
|
|
def http_monitor(self):
|
|
result = dict()
|
|
result['ltm:monitor:http:ea4346e49cdf'] = [
|
|
dict(
|
|
parameters=dict(
|
|
name='monitor-http'
|
|
)
|
|
)
|
|
]
|
|
return result
|
|
|
|
@property
|
|
def nodes(self):
|
|
result = dict()
|
|
result['ltm:node:9e76a6323321'] = []
|
|
for x in self.servers:
|
|
tmp = dict(
|
|
parameters=dict(
|
|
name=x['address'],
|
|
address=x['address']
|
|
)
|
|
)
|
|
result['ltm:node:9e76a6323321'].append(tmp)
|
|
return result
|
|
|
|
@property
|
|
def node_addresses(self):
|
|
result = [x['address'] for x in self.servers]
|
|
return result
|
|
|
|
|
|
class ReportableChanges(Changes):
|
|
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
|
|
|
|
|
|
class ModuleManager(object):
|
|
def __init__(self, *args, **kwargs):
|
|
self.module = kwargs.get('module', None)
|
|
self.client = kwargs.get('client', None)
|
|
self.want = ModuleParameters(params=self.module.params)
|
|
self.want.client = self.client
|
|
self.have = ApiParameters()
|
|
self.changes = UsableChanges()
|
|
|
|
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 = UsableChanges(params=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:
|
|
if isinstance(change, dict):
|
|
changed.update(change)
|
|
else:
|
|
changed[k] = change
|
|
if changed:
|
|
self.changes = UsableChanges(params=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
|
|
|
|
if state == "present":
|
|
changed = self.present()
|
|
elif state == "absent":
|
|
changed = self.absent()
|
|
|
|
reportable = ReportableChanges(params=self.changes.to_return())
|
|
changes = reportable.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 present(self):
|
|
if self.exists():
|
|
return False
|
|
else:
|
|
return self.create()
|
|
|
|
def exists(self):
|
|
uri = "https://{0}:{1}/mgmt/ap/query/v1/tenants/default/reports/AllApplicationsList?$filter=name+eq+'{2}'".format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port'],
|
|
self.want.name
|
|
)
|
|
resp = self.client.api.get(uri)
|
|
try:
|
|
response = resp.json()
|
|
except ValueError as ex:
|
|
raise F5ModuleError(str(ex))
|
|
if resp.status == 200 and 'result' in response and 'totalItems' in response['result'] and response['result']['totalItems'] == 0:
|
|
return False
|
|
return True
|
|
|
|
def remove(self):
|
|
if self.module.check_mode:
|
|
return True
|
|
self_link = self.remove_from_device()
|
|
if self.want.wait:
|
|
self.wait_for_apply_template_task(self_link)
|
|
if self.exists():
|
|
raise F5ModuleError("Failed to delete the resource.")
|
|
return True
|
|
|
|
def has_no_service_environment(self):
|
|
if self.want.default_device_reference is None and self.want.ssg_reference is None:
|
|
return True
|
|
return False
|
|
|
|
def create(self):
|
|
if self.want.service_environment is None:
|
|
raise F5ModuleError(
|
|
"A 'service_environment' must be specified when creating a new application."
|
|
)
|
|
if self.want.servers is None:
|
|
raise F5ModuleError(
|
|
"At least one 'servers' item is needed when creating a new application."
|
|
)
|
|
if self.want.inbound_virtual is None:
|
|
raise F5ModuleError(
|
|
"An 'inbound_virtual' must be specified when creating a new application."
|
|
)
|
|
self._set_changed_options()
|
|
|
|
if self.has_no_service_environment():
|
|
raise F5ModuleError(
|
|
"The specified 'service_environment' ({0}) was not found.".format(self.want.service_environment)
|
|
)
|
|
|
|
if self.module.check_mode:
|
|
return True
|
|
self_link = self.create_on_device()
|
|
if self.want.wait:
|
|
self.wait_for_apply_template_task(self_link)
|
|
if not self.exists():
|
|
raise F5ModuleError(
|
|
"Failed to deploy application."
|
|
)
|
|
return True
|
|
|
|
def create_on_device(self):
|
|
params = self.changes.api_params()
|
|
params['mode'] = 'CREATE'
|
|
|
|
uri = 'https://{0}:{1}/mgmt/cm/global/tasks/apply-template'.format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port']
|
|
)
|
|
|
|
resp = self.client.api.post(uri, json=params)
|
|
try:
|
|
response = resp.json()
|
|
except ValueError as ex:
|
|
raise F5ModuleError(str(ex))
|
|
|
|
if 'code' in response and response['code'] == 400:
|
|
if 'message' in response:
|
|
raise F5ModuleError(response['message'])
|
|
else:
|
|
raise F5ModuleError(resp._content)
|
|
return response['selfLink']
|
|
|
|
def absent(self):
|
|
if self.exists():
|
|
return self.remove()
|
|
return False
|
|
|
|
def remove_from_device(self):
|
|
params = dict(
|
|
configSetName=self.want.name,
|
|
mode='DELETE'
|
|
)
|
|
uri = 'https://{0}:{1}/mgmt/cm/global/tasks/apply-template'.format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port']
|
|
)
|
|
|
|
resp = self.client.api.post(uri, json=params)
|
|
try:
|
|
response = resp.json()
|
|
except ValueError as ex:
|
|
raise F5ModuleError(str(ex))
|
|
|
|
if 'code' in response and response['code'] == 400:
|
|
if 'message' in response:
|
|
raise F5ModuleError(response['message'])
|
|
else:
|
|
raise F5ModuleError(resp._content)
|
|
return response['selfLink']
|
|
|
|
def wait_for_apply_template_task(self, self_link):
|
|
host = 'https://{0}:{1}'.format(
|
|
self.client.provider['server'],
|
|
self.client.provider['server_port']
|
|
)
|
|
uri = self_link.replace('https://localhost', host)
|
|
|
|
while True:
|
|
resp = self.client.api.get(uri)
|
|
try:
|
|
response = resp.json()
|
|
except ValueError as ex:
|
|
raise F5ModuleError(str(ex))
|
|
|
|
if response['status'] == 'FINISHED' and response.get('currentStep', None) == 'DONE':
|
|
return True
|
|
elif 'errorMessage' in response:
|
|
raise F5ModuleError(response['errorMessage'])
|
|
time.sleep(5)
|
|
|
|
|
|
class ArgumentSpec(object):
|
|
def __init__(self):
|
|
self.supports_check_mode = True
|
|
argument_spec = dict(
|
|
name=dict(required=True),
|
|
description=dict(),
|
|
servers=dict(
|
|
type='list',
|
|
options=dict(
|
|
address=dict(required=True),
|
|
port=dict(default=80)
|
|
)
|
|
),
|
|
inbound_virtual=dict(
|
|
type='dict',
|
|
options=dict(
|
|
address=dict(required=True),
|
|
netmask=dict(required=True),
|
|
port=dict(default=80)
|
|
)
|
|
),
|
|
service_environment=dict(),
|
|
add_analytics=dict(type='bool', default='no'),
|
|
state=dict(
|
|
default='present',
|
|
choices=['present', 'absent']
|
|
),
|
|
wait=dict(type='bool', default='yes')
|
|
)
|
|
self.argument_spec = {}
|
|
self.argument_spec.update(f5_argument_spec)
|
|
self.argument_spec.update(argument_spec)
|
|
|
|
|
|
def main():
|
|
spec = ArgumentSpec()
|
|
|
|
module = AnsibleModule(
|
|
argument_spec=spec.argument_spec,
|
|
supports_check_mode=spec.supports_check_mode
|
|
)
|
|
if not HAS_NETADDR:
|
|
module.fail_json(msg="The python netaddr module is required")
|
|
|
|
try:
|
|
client = F5RestClient(module=module)
|
|
mm = ModuleManager(module=module, client=client)
|
|
results = mm.exec_module()
|
|
exit_json(module, results, client)
|
|
except F5ModuleError as ex:
|
|
fail_json(module, ex, client)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|