[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)),
|
location_type, location, location_type, link)),
|
||||||
changed=False)
|
changed=False)
|
||||||
return l
|
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