mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
[cloud][GCP]: New module gcp_backend_service for load balancer backends (#22857)
* GCP: backend service module * GCP: rework param-checking code. Fixed a couple of bugs and changed to ValueError instead of custom tuple. * GCP: fixed commit, spelled out Google Cloud for clarity in module description.
This commit is contained in:
parent
2bf7297bf3
commit
8b677e25c4
2 changed files with 461 additions and 0 deletions
|
@ -413,3 +413,44 @@ def get_valid_location(module, driver, location, location_type='zone'):
|
|||
location_type, location, location_type, link)),
|
||||
changed=False)
|
||||
return l
|
||||
|
||||
def check_params(params, field_list):
|
||||
"""
|
||||
Helper to validate params.
|
||||
|
||||
Use this in function definitions if they require specific fields
|
||||
to be present.
|
||||
|
||||
:param params: structure that contains the fields
|
||||
:type params: ``dict``
|
||||
|
||||
:param field_list: list of dict representing the fields
|
||||
[{'name': str, 'required': True/False', 'type': cls}]
|
||||
:type field_list: ``list`` of ``dict``
|
||||
|
||||
:return True or raises ValueError
|
||||
:rtype: ``bool`` or `class:ValueError`
|
||||
"""
|
||||
for d in field_list:
|
||||
if not d['name'] in params:
|
||||
if 'required' in d and d['required'] is True:
|
||||
raise ValueError(("%s is required and must be of type: %s" %
|
||||
(d['name'], str(d['type']))))
|
||||
else:
|
||||
if not isinstance(params[d['name']], d['type']):
|
||||
raise ValueError(("%s must be of type: %s" % (
|
||||
d['name'], str(d['type']))))
|
||||
if 'values' in d:
|
||||
if params[d['name']] not in d['values']:
|
||||
raise ValueError(("%s must be one of: %s" % (
|
||||
d['name'], ','.join(d['values']))))
|
||||
if isinstance(params[d['name']], int):
|
||||
if 'min' in d:
|
||||
if params[d['name']] < d['min']:
|
||||
raise ValueError(("%s must be greater than or equal to: %s" % (
|
||||
d['name'], d['min'])))
|
||||
if 'max' in d:
|
||||
if params[d['name']] > d['max']:
|
||||
raise ValueError("%s must be less than or equal to: %s" % (
|
||||
d['name'], d['max']))
|
||||
return True
|
||||
|
|
420
lib/ansible/modules/cloud/google/gcp_backend_service.py
Normal file
420
lib/ansible/modules/cloud/google/gcp_backend_service.py
Normal file
|
@ -0,0 +1,420 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright 2017 Google 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 = {'metadata_version': '1.0',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
DOCUMENTATION = '''
|
||||
module: gcp_backend_service
|
||||
version_added: "2.4"
|
||||
short_description: Create or Destroy a Backend Service.
|
||||
description:
|
||||
- Create or Destroy a Backend Service. See
|
||||
U(https://cloud.google.com/compute/docs/load-balancing/http/backend-service) for an overview.
|
||||
Full install/configuration instructions for the Google Cloud modules can
|
||||
be found in the comments of ansible/test/gce_tests.py.
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "apache-libcloud >= 1.3.0"
|
||||
notes:
|
||||
- Update is not currently supported.
|
||||
- Only global backend services are currently supported. Regional backends not currently supported.
|
||||
- Internal load balancing not currently supported.
|
||||
author:
|
||||
- "Tom Melendez (@supertom) <tom@supertom.com>"
|
||||
options:
|
||||
backend_service_name:
|
||||
description:
|
||||
- Name of the Backend Service.
|
||||
required: true
|
||||
backends:
|
||||
description:
|
||||
- List of backends that make up the backend service. A backend is made up of
|
||||
an instance group and optionally several other parameters. See
|
||||
U(https://cloud.google.com/compute/docs/reference/latest/backendServices)
|
||||
for details.
|
||||
required: true
|
||||
healthchecks:
|
||||
description:
|
||||
- List of healthchecks. Only one healthcheck is supported.
|
||||
required: true
|
||||
enable_cdn:
|
||||
description:
|
||||
- If true, enable Cloud CDN for this Backend Service.
|
||||
required: false
|
||||
port_name:
|
||||
description:
|
||||
- Name of the port on the managed instance group (MIG) that backend
|
||||
services can forward data to. Required for external load balancing.
|
||||
required: false
|
||||
default: null
|
||||
protocol:
|
||||
description:
|
||||
- The protocol this Backend Service uses to communicate with backends.
|
||||
Possible values are HTTP, HTTPS, TCP, and SSL. The default is HTTP.
|
||||
required: false
|
||||
timeout:
|
||||
description:
|
||||
- How many seconds to wait for the backend before considering it a failed
|
||||
request. Default is 30 seconds. Valid range is 1-86400.
|
||||
required: false
|
||||
service_account_email:
|
||||
description:
|
||||
- Service account email
|
||||
required: false
|
||||
default: null
|
||||
credentials_file:
|
||||
description:
|
||||
- Path to the JSON file associated with the service account email.
|
||||
default: null
|
||||
required: false
|
||||
project_id:
|
||||
description:
|
||||
- GCE project ID.
|
||||
required: false
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- Desired state of the resource
|
||||
required: false
|
||||
default: "present"
|
||||
choices: ["absent", "present"]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Create Minimum Backend Service
|
||||
gcp_backend_service:
|
||||
service_account_email: "{{ service_account_email }}"
|
||||
credentials_file: "{{ credentials_file }}"
|
||||
project_id: "{{ project_id }}"
|
||||
backend_service_name: "{{ bes }}"
|
||||
backends:
|
||||
- instance_group: managed_instance_group_1
|
||||
healthchecks:
|
||||
- name: healthcheck_name_for_backend_service
|
||||
port_name: myhttpport
|
||||
state: present
|
||||
|
||||
- name: Create BES with extended backend parameters
|
||||
gcp_backend_service:
|
||||
service_account_email: "{{ service_account_email }}"
|
||||
credentials_file: "{{ credentials_file }}"
|
||||
project_id: "{{ project_id }}"
|
||||
backend_service_name: "{{ bes }}"
|
||||
backends:
|
||||
- instance_group: managed_instance_group_1
|
||||
max_utilization: 0.6
|
||||
max_rate: 10
|
||||
- instance_group: managed_instance_group_2
|
||||
max_utilization: 0.5
|
||||
max_rate: 4
|
||||
healthchecks:
|
||||
- name: healthcheck_name_for_backend_service
|
||||
port_name: myhttpport
|
||||
state: present
|
||||
timeout: 60
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
backend_service_created:
|
||||
description: Indicator Backend Service was created.
|
||||
returned: When a Backend Service is created.
|
||||
type: boolean
|
||||
sample: "True"
|
||||
backend_service_deleted:
|
||||
description: Indicator Backend Service was deleted.
|
||||
returned: When a Backend Service is deleted.
|
||||
type: boolean
|
||||
sample: "True"
|
||||
backend_service_name:
|
||||
description: Name of the Backend Service.
|
||||
returned: Always.
|
||||
type: string
|
||||
sample: "my-backend-service"
|
||||
backends:
|
||||
description: List of backends (comprised of instance_group) that
|
||||
make up a Backend Service.
|
||||
returned: When a Backend Service exists.
|
||||
type: list
|
||||
sample: "[ { 'instance_group': 'mig_one', 'zone': 'us-central1-b'} ]"
|
||||
enable_cdn:
|
||||
description: If Cloud CDN is enabled. null if not set.
|
||||
returned: When a backend service exists.
|
||||
type: boolean
|
||||
sample: "True"
|
||||
healthchecks:
|
||||
description: List of healthchecks applied to the Backend Service.
|
||||
returned: When a Backend Service exists.
|
||||
type: list
|
||||
sample: "[ 'my-healthcheck' ]"
|
||||
protocol:
|
||||
description: Protocol used to communicate with the Backends.
|
||||
returned: When a Backend Service exists.
|
||||
type: string
|
||||
sample: "HTTP"
|
||||
port_name:
|
||||
description: Name of Backend Port.
|
||||
returned: When a Backend Service exists.
|
||||
type: string
|
||||
sample: "myhttpport"
|
||||
timeout:
|
||||
description: In seconds, how long before a request sent to a backend is
|
||||
considered failed.
|
||||
returned: If specified.
|
||||
type: integer
|
||||
sample: "myhttpport"
|
||||
'''
|
||||
|
||||
|
||||
try:
|
||||
import libcloud
|
||||
from libcloud.compute.types import Provider
|
||||
from libcloud.compute.providers import get_driver
|
||||
from libcloud.common.google import GoogleBaseError, QuotaExceededError, \
|
||||
ResourceExistsError, ResourceInUseError, ResourceNotFoundError
|
||||
from libcloud.compute.drivers.gce import GCEAddress
|
||||
_ = Provider.GCE
|
||||
HAS_LIBCLOUD = True
|
||||
except ImportError:
|
||||
HAS_LIBCLOUD = False
|
||||
|
||||
try:
|
||||
from ast import literal_eval
|
||||
HAS_PYTHON26 = True
|
||||
except ImportError:
|
||||
HAS_PYTHON26 = False
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.gce import gce_connect
|
||||
from ansible.module_utils.gcp import check_params
|
||||
|
||||
|
||||
def _validate_params(params):
|
||||
"""
|
||||
Validate backend_service params.
|
||||
|
||||
This function calls _validate_backend_params to verify
|
||||
the backend-specific parameters.
|
||||
|
||||
:param params: Ansible dictionary containing configuration.
|
||||
:type params: ``dict``
|
||||
|
||||
:return: True or raises ValueError
|
||||
:rtype: ``bool`` or `class:ValueError`
|
||||
"""
|
||||
fields = [
|
||||
{'name': 'timeout', 'type': int, 'min': 1, 'max': 86400},
|
||||
]
|
||||
try:
|
||||
check_params(params, fields)
|
||||
_validate_backend_params(params['backends'])
|
||||
except:
|
||||
raise
|
||||
|
||||
return (True, '')
|
||||
|
||||
|
||||
def _validate_backend_params(backends):
|
||||
"""
|
||||
Validate configuration for backends.
|
||||
|
||||
:param backends: Ansible dictionary containing backends configuration (only).
|
||||
:type backends: ``dict``
|
||||
|
||||
:return: True or raises ValueError
|
||||
:rtype: ``bool`` or `class:ValueError`
|
||||
"""
|
||||
fields = [
|
||||
{'name': 'balancing_mode', 'type': str, 'values': ['UTILIZATION', 'RATE', 'CONNECTION']},
|
||||
{'name': 'max_utilization', 'type': float},
|
||||
{'name': 'max_connections', 'type': int},
|
||||
{'name': 'max_rate', 'type': int},
|
||||
{'name': 'max_rate_per_instance', 'type': float},
|
||||
]
|
||||
|
||||
if not backends:
|
||||
raise ValueError('backends should be a list.')
|
||||
|
||||
for backend in backends:
|
||||
try:
|
||||
check_params(backend, fields)
|
||||
except:
|
||||
raise
|
||||
|
||||
if 'max_rate' in backend and 'max_rate_per_instance' in backend:
|
||||
raise ValueError('Both maxRate or maxRatePerInstance cannot be set.')
|
||||
|
||||
return (True, '')
|
||||
|
||||
|
||||
def get_backend_service(gce, name):
|
||||
"""
|
||||
Get a Backend Service from GCE.
|
||||
|
||||
:param gce: An initialized GCE driver object.
|
||||
:type gce: :class: `GCENodeDriver`
|
||||
|
||||
:param name: Name of the Backend Service.
|
||||
:type name: ``str``
|
||||
|
||||
:return: A GCEBackendService object or None.
|
||||
:rtype: :class: `GCEBackendService` or None
|
||||
"""
|
||||
try:
|
||||
# Does the Backend Service already exist?
|
||||
return gce.ex_get_backendservice(name=name)
|
||||
|
||||
except ResourceNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def get_healthcheck(gce, name):
|
||||
return gce.ex_get_healthcheck(name)
|
||||
|
||||
|
||||
def get_instancegroup(gce, name, zone=None):
|
||||
return gce.ex_get_instancegroup(name=name, zone=zone)
|
||||
|
||||
|
||||
def create_backend_service(gce, params):
|
||||
"""
|
||||
Create a new Backend Service.
|
||||
|
||||
:param gce: An initialized GCE driver object.
|
||||
:type gce: :class: `GCENodeDriver`
|
||||
|
||||
:param params: Dictionary of parameters needed by the module.
|
||||
:type params: ``dict``
|
||||
|
||||
:return: Tuple with changed stats
|
||||
:rtype: tuple in the format of (bool, bool)
|
||||
"""
|
||||
from copy import deepcopy
|
||||
|
||||
changed = False
|
||||
return_data = False
|
||||
# only one healthcheck is currently supported
|
||||
hc_name = params['healthchecks'][0]
|
||||
hc = get_healthcheck(gce, hc_name)
|
||||
backends = []
|
||||
for backend in params['backends']:
|
||||
ig = get_instancegroup(gce, backend['instance_group'],
|
||||
backend.get('zone', None))
|
||||
kwargs = deepcopy(backend)
|
||||
kwargs['instance_group'] = ig
|
||||
backends.append(gce.ex_create_backend(
|
||||
**kwargs))
|
||||
|
||||
bes = gce.ex_create_backendservice(
|
||||
name=params['backend_service_name'], healthchecks=[hc], backends=backends,
|
||||
enable_cdn=params['enable_cdn'], port_name=params['port_name'],
|
||||
timeout_sec=params['timeout'], protocol=params['protocol'])
|
||||
|
||||
if bes:
|
||||
changed = True
|
||||
return_data = True
|
||||
|
||||
return (changed, return_data)
|
||||
|
||||
|
||||
def delete_backend_service(bes):
|
||||
"""
|
||||
Delete a Backend Service. The Instance Groups are NOT destroyed.
|
||||
"""
|
||||
changed = False
|
||||
return_data = False
|
||||
if bes.destroy():
|
||||
changed = True
|
||||
return_data = True
|
||||
return (changed, return_data)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(argument_spec=dict(
|
||||
backends=dict(type='list', required=True),
|
||||
backend_service_name=dict(required=True),
|
||||
healthchecks=dict(type='list', required=True),
|
||||
service_account_email=dict(),
|
||||
service_account_permissions=dict(type='list'),
|
||||
enable_cdn=dict(type='bool', choices=[True, False]),
|
||||
port_name=dict(type='str'),
|
||||
protocol=dict(type='str', default='TCP',
|
||||
choices=['HTTP', 'HTTPS', 'SSL', 'TCP']),
|
||||
timeout=dict(type='int'),
|
||||
state=dict(choices=['absent', 'present'], default='present'),
|
||||
pem_file=dict(),
|
||||
credentials_file=dict(),
|
||||
project_id=dict(), ), )
|
||||
|
||||
if not HAS_PYTHON26:
|
||||
module.fail_json(
|
||||
msg="GCE module requires python's 'ast' module, python v2.6+")
|
||||
if not HAS_LIBCLOUD:
|
||||
module.fail_json(
|
||||
msg='libcloud with GCE Backend Service support (1.3+) required for this module.')
|
||||
|
||||
gce = gce_connect(module)
|
||||
if not hasattr(gce, 'ex_create_instancegroupmanager'):
|
||||
module.fail_json(
|
||||
msg='libcloud with GCE Backend Service support (1.3+) required for this module.',
|
||||
changed=False)
|
||||
|
||||
params = {}
|
||||
params['state'] = module.params.get('state')
|
||||
params['backend_service_name'] = module.params.get('backend_service_name')
|
||||
params['backends'] = module.params.get('backends')
|
||||
params['healthchecks'] = module.params.get('healthchecks')
|
||||
params['enable_cdn'] = module.params.get('enable_cdn', None)
|
||||
params['port_name'] = module.params.get('port_name', None)
|
||||
params['protocol'] = module.params.get('protocol', None)
|
||||
params['timeout'] = module.params.get('timeout', None)
|
||||
|
||||
try:
|
||||
_validate_params(params)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=e.message, changed=False)
|
||||
|
||||
changed = False
|
||||
json_output = {'state': params['state']}
|
||||
bes = get_backend_service(gce, params['backend_service_name'])
|
||||
|
||||
if not bes:
|
||||
if params['state'] == 'absent':
|
||||
# Doesn't exist and state==absent.
|
||||
changed = False
|
||||
module.fail_json(
|
||||
msg="Cannot delete unknown backend service: %s" %
|
||||
(params['backend_service_name']))
|
||||
else:
|
||||
# Create
|
||||
(changed, json_output['backend_service_created']) = create_backend_service(gce,
|
||||
params)
|
||||
elif params['state'] == 'absent':
|
||||
# Delete
|
||||
(changed, json_output['backend_service_deleted']) = delete_backend_service(bes)
|
||||
else:
|
||||
# TODO(supertom): Add update support when it is available in libcloud.
|
||||
changed = False
|
||||
|
||||
json_output['changed'] = changed
|
||||
json_output.update(params)
|
||||
module.exit_json(**json_output)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Loading…
Reference in a new issue