diff --git a/lib/ansible/module_utils/gcp.py b/lib/ansible/module_utils/gcp.py
index c0f2ce0f83a..c3b0d2bdd3b 100644
--- a/lib/ansible/module_utils/gcp.py
+++ b/lib/ansible/module_utils/gcp.py
@@ -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
diff --git a/lib/ansible/modules/cloud/google/gcp_backend_service.py b/lib/ansible/modules/cloud/google/gcp_backend_service.py
new file mode 100644
index 00000000000..99fe87791a8
--- /dev/null
+++ b/lib/ansible/modules/cloud/google/gcp_backend_service.py
@@ -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 .
+
+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) "
+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()