[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:
Tom Melendez 2017-03-30 13:59:35 -07:00 committed by Ryan Brown
parent 2bf7297bf3
commit 8b677e25c4
2 changed files with 461 additions and 0 deletions

View file

@ -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

View 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()