diff --git a/lib/ansible/module_utils/gcp.py b/lib/ansible/module_utils/gcp.py
index c3b0d2bdd3..441462bab7 100644
--- a/lib/ansible/module_utils/gcp.py
+++ b/lib/ansible/module_utils/gcp.py
@@ -29,6 +29,7 @@
import json
import os
+import time
import traceback
from distutils.version import LooseVersion
@@ -44,7 +45,7 @@ try:
import google.auth
from google.oauth2 import service_account
HAS_GOOGLE_AUTH = True
-except ImportError as e:
+except ImportError:
HAS_GOOGLE_AUTH = False
# google-python-api
@@ -52,6 +53,8 @@ try:
import google_auth_httplib2
from httplib2 import Http
from googleapiclient.http import set_user_agent
+ from googleapiclient.errors import HttpError
+ from apiclient.discovery import build
HAS_GOOGLE_API_LIB = True
except ImportError:
HAS_GOOGLE_API_LIB = False
@@ -64,6 +67,11 @@ except ImportError:
from ansible.utils.display import Display
display = Display()
+import ansible.module_utils.six.moves.urllib.parse as urlparse
+
+GCP_DEFAULT_SCOPES = ['https://www.googleapis.com/auth/cloud-platform']
+
+
def _get_gcp_ansible_credentials(module):
"""Helper to fetch creds from AnsibleModule object."""
service_account_email = module.params.get('service_account_email', None)
@@ -74,11 +82,13 @@ def _get_gcp_ansible_credentials(module):
return (service_account_email, credentials_file, project_id)
+
def _get_gcp_environ_var(var_name, default_value):
"""Wrapper around os.environ.get call."""
return os.environ.get(
var_name, default_value)
+
def _get_gcp_environment_credentials(service_account_email, credentials_file, project_id):
"""Helper to look in environment variables for credentials."""
# If any of the values are not given as parameters, check the appropriate
@@ -95,6 +105,7 @@ def _get_gcp_environment_credentials(service_account_email, credentials_file, pr
'GOOGLE_CLOUD_PROJECT', None)
return (service_account_email, credentials_file, project_id)
+
def _get_gcp_libcloud_credentials(service_account_email=None, credentials_file=None, project_id=None):
"""
Helper to look for libcloud secrets.py file.
@@ -134,6 +145,7 @@ def _get_gcp_libcloud_credentials(service_account_email=None, credentials_file=N
project_id = keyword_params.get('project', None)
return (service_account_email, credentials_file, project_id)
+
def _get_gcp_credentials(module, require_valid_json=True, check_libcloud=False):
"""
Obtain GCP credentials by trying various methods.
@@ -193,13 +205,14 @@ def _get_gcp_credentials(module, require_valid_json=True, check_libcloud=False):
if credentials_file is None or project_id is None or service_account_email is None:
if check_libcloud is True:
if project_id is None:
- # TODO(supertom): this message is legacy and integration tests depend on it.
+ # TODO(supertom): this message is legacy and integration tests
+ # depend on it.
module.fail_json(msg='Missing GCE connection parameters in libcloud '
'secrets file.')
else:
if project_id is None:
module.fail_json(msg=('GCP connection error: unable to determine project (%s) or '
- 'credentials file (%s)' % (project_id, credentials_file)))
+ 'credentials file (%s)' % (project_id, credentials_file)))
# Set these fields to empty strings if they are None
# consumers of this will make the distinction between an empty string
# and None.
@@ -218,6 +231,7 @@ def _get_gcp_credentials(module, require_valid_json=True, check_libcloud=False):
'credentials_file': credentials_file,
'project_id': project_id}
+
def _validate_credentials_file(module, credentials_file, require_valid_json=True, check_libcloud=False):
"""
Check for valid credentials file.
@@ -245,17 +259,20 @@ def _validate_credentials_file(module, credentials_file, require_valid_json=True
with open(credentials_file) as credentials:
json.loads(credentials.read())
# If the credentials are proper JSON and we do not have the minimum
- # required libcloud version, bail out and return a descriptive error
+ # required libcloud version, bail out and return a descriptive
+ # error
if check_libcloud and LooseVersion(libcloud.__version__) < '0.17.0':
module.fail_json(msg='Using JSON credentials but libcloud minimum version not met. '
'Upgrade to libcloud>=0.17.0.')
return True
except IOError as e:
- module.fail_json(msg='GCP Credentials File %s not found.' % credentials_file, changed=False)
+ module.fail_json(msg='GCP Credentials File %s not found.' %
+ credentials_file, changed=False)
return False
except ValueError as e:
if require_valid_json:
- module.fail_json(msg='GCP Credentials File %s invalid. Must be valid JSON.' % credentials_file, changed=False)
+ module.fail_json(
+ msg='GCP Credentials File %s invalid. Must be valid JSON.' % credentials_file, changed=False)
else:
display.deprecated(msg=("Non-JSON credentials file provided. This format is deprecated. "
" Please generate a new JSON key from the Google Cloud console"),
@@ -273,7 +290,7 @@ def gcp_connect(module, provider, get_driver, user_agent_product, user_agent_ver
check_libcloud=True)
try:
gcp = get_driver(provider)(creds['service_account_email'], creds['credentials_file'],
- datacenter=module.params.get('zone', None),
+ datacenter=module.params.get('zone', None),
project=creds['project_id'])
gcp.connection.user_agent_append("%s/%s" % (
user_agent_product, user_agent_version))
@@ -318,8 +335,8 @@ def get_google_cloud_credentials(module, scopes=[]):
module.fail_json(msg='Please install google-auth.')
conn_params = _get_gcp_credentials(module,
- require_valid_json=True,
- check_libcloud=False)
+ require_valid_json=True,
+ check_libcloud=False)
try:
if conn_params['credentials_file']:
credentials = service_account.Credentials.from_service_account_file(
@@ -337,6 +354,7 @@ def get_google_cloud_credentials(module, scopes=[]):
module.fail_json(msg=unexpected_error_msg(e), changed=False)
return (None, None)
+
def get_google_api_auth(module, scopes=[], user_agent_product='ansible-python-api', user_agent_version='NA'):
"""
Authentication for use with google-python-api-client.
@@ -375,12 +393,12 @@ def get_google_api_auth(module, scopes=[], user_agent_product='ansible-python-ap
"""
if not HAS_GOOGLE_API_LIB:
module.fail_json(msg="Please install google-api-python-client library")
- # TODO(supertom): verify scopes
if not scopes:
- scopes = ['https://www.googleapis.com/auth/cloud-platform']
+ scopes = GCP_DEFAULT_SCOPES
try:
(credentials, conn_params) = get_google_cloud_credentials(module, scopes)
- http = set_user_agent(Http(), '%s-%s' % (user_agent_product, user_agent_version))
+ http = set_user_agent(Http(), '%s-%s' %
+ (user_agent_product, user_agent_version))
http_auth = google_auth_httplib2.AuthorizedHttp(credentials, http=http)
return (http_auth, conn_params)
@@ -388,6 +406,30 @@ def get_google_api_auth(module, scopes=[], user_agent_product='ansible-python-ap
module.fail_json(msg=unexpected_error_msg(e), changed=False)
return (None, None)
+
+def get_google_api_client(module, service, user_agent_product, user_agent_version,
+ scopes=None, api_version='v1'):
+ """
+ Get the discovery-based python client. Use when a cloud client is not available.
+
+ client = get_google_api_client(module, 'compute', user_agent_product=USER_AGENT_PRODUCT,
+ user_agent_version=USER_AGENT_VERSION)
+
+ :returns: A tuple containing the authorized client to the specified service and a
+ params dict {'service_account_email': '...', 'credentials_file': '...', 'project_id': ...}
+ :rtype: ``tuple``
+ """
+ if not scopes:
+ scopes = GCP_DEFAULT_SCOPES
+
+ http_auth, conn_params = get_google_api_auth(module, scopes=scopes,
+ user_agent_product=user_agent_product,
+ user_agent_version=user_agent_version)
+ client = build(service, api_version, http=http_auth)
+
+ return (client, conn_params)
+
+
def check_min_pkg_version(pkg_name, minimum_version):
"""Minimum required version is >= installed version."""
from pkg_resources import get_distribution
@@ -397,10 +439,12 @@ def check_min_pkg_version(pkg_name, minimum_version):
except Exception as e:
return False
+
def unexpected_error_msg(error):
"""Create an error string based on passed in error."""
return 'Unexpected response: (%s). Detail: %s' % (str(error), traceback.format_exc())
+
def get_valid_location(module, driver, location, location_type='zone'):
if location_type == 'zone':
l = driver.ex_get_zone(location)
@@ -414,6 +458,7 @@ def get_valid_location(module, driver, location, location_type='zone'):
changed=False)
return l
+
def check_params(params, field_list):
"""
Helper to validate params.
@@ -435,11 +480,12 @@ def check_params(params, 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']))))
+ (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']))))
+ raise ValueError(("%s must be of type: %s. %s (%s) provided." % (
+ d['name'], str(d['type']), params[d['name']],
+ type(params[d['name']]))))
if 'values' in d:
if params[d['name']] not in d['values']:
raise ValueError(("%s must be one of: %s" % (
@@ -454,3 +500,372 @@ def check_params(params, field_list):
raise ValueError("%s must be less than or equal to: %s" % (
d['name'], d['max']))
return True
+
+
+class GCPUtils(object):
+ """
+ Helper utilities for GCP.
+ """
+
+ @staticmethod
+ def underscore_to_camel(txt):
+ return txt.split('_')[0] + ''.join(x.capitalize()
+ or '_' for x in txt.split('_')[1:])
+
+ @staticmethod
+ def remove_non_gcp_params(params):
+ """
+ Remove params if found.
+ """
+ params_to_remove = ['state']
+ for p in params_to_remove:
+ if p in params:
+ del params[p]
+
+ return params
+
+ @staticmethod
+ def params_to_gcp_dict(params, resource_name=None):
+ """
+ Recursively convert ansible params to GCP Params.
+
+ Keys are converted from snake to camelCase
+ ex: default_service to defaultService
+
+ Handles lists, dicts and strings
+
+ special provision for the resource name
+ """
+ if not isinstance(params, dict):
+ return params
+ gcp_dict = {}
+ params = GCPUtils.remove_non_gcp_params(params)
+ for k, v in params.items():
+ gcp_key = GCPUtils.underscore_to_camel(k)
+ if isinstance(v, dict):
+ retval = GCPUtils.params_to_gcp_dict(v)
+ gcp_dict[gcp_key] = retval
+ elif isinstance(v, list):
+ gcp_dict[gcp_key] = [GCPUtils.params_to_gcp_dict(x) for x in v]
+ else:
+ if resource_name and k == resource_name:
+ gcp_dict['name'] = v
+ else:
+ gcp_dict[gcp_key] = v
+ return gcp_dict
+
+ @staticmethod
+ def execute_api_client_req(req, client=None, raw=True,
+ operation_timeout=180, poll_interval=5,
+ raise_404=True):
+ """
+ General python api client interaction function.
+
+ For use with google-api-python-client, or clients created
+ with get_google_api_client function
+ Not for use with Google Cloud client libraries
+
+ For long-running operations, we make an immediate query and then
+ sleep poll_interval before re-querying. After the request is done
+ we rebuild the request with a get method and return the result.
+
+ """
+ try:
+ resp = req.execute()
+
+ if not resp:
+ return None
+
+ if raw:
+ return resp
+
+ if resp['kind'] == 'compute#operation':
+ resp = GCPUtils.execute_api_client_operation_req(req, resp,
+ client,
+ operation_timeout,
+ poll_interval)
+
+ if 'items' in resp:
+ return resp['items']
+
+ return resp
+ except HttpError as h:
+ # Note: 404s can be generated (incorrectly) for dependent
+ # resources not existing. We let the caller determine if
+ # they want 404s raised for their invocation.
+ if h.resp.status == 404 and not raise_404:
+ return None
+ else:
+ raise
+ except Exception:
+ raise
+
+ @staticmethod
+ def execute_api_client_operation_req(orig_req, op_resp, client,
+ operation_timeout=180, poll_interval=5):
+ """
+ Poll an operation for a result.
+ """
+ parsed_url = GCPUtils.parse_gcp_url(orig_req.uri)
+ project_id = parsed_url['project']
+ resource_name = GCPUtils.get_gcp_resource_from_methodId(
+ orig_req.methodId)
+ resource = GCPUtils.build_resource_from_name(client, resource_name)
+
+ start_time = time.time()
+
+ complete = False
+ attempts = 1
+ while not complete:
+ if start_time + operation_timeout >= time.time():
+ op_req = client.globalOperations().get(
+ project=project_id, operation=op_resp['name'])
+ op_resp = op_req.execute()
+ if op_resp['status'] != 'DONE':
+ time.sleep(poll_interval)
+ attempts += 1
+ else:
+ complete = True
+ if op_resp['operationType'] == 'delete':
+ # don't wait for the delete
+ return True
+ elif op_resp['operationType'] in ['insert', 'update', 'patch']:
+ # TODO(supertom): Isolate 'build-new-request' stuff.
+ resource_name_singular = GCPUtils.get_entity_name_from_resource_name(
+ resource_name)
+ if op_resp['operationType'] == 'insert' or not 'entity_name' in parsed_url:
+ parsed_url['entity_name'] = GCPUtils.parse_gcp_url(op_resp['targetLink'])[
+ 'entity_name']
+ args = {'project': project_id,
+ resource_name_singular: parsed_url['entity_name']}
+ new_req = resource.get(**args)
+ resp = new_req.execute()
+ return resp
+ else:
+ # assuming multiple entities, do a list call.
+ new_req = resource.list(project=project_id)
+ resp = new_req.execute()
+ return resp
+ else:
+ # operation didn't complete on time.
+ raise GCPOperationTimeoutError("Operation timed out: %s" % (
+ op_resp['targetLink']))
+
+ @staticmethod
+ def build_resource_from_name(client, resource_name):
+ try:
+ method = getattr(client, resource_name)
+ return method()
+ except AttributeError:
+ raise NotImplementedError('%s is not an attribute of %s' % (resource_name,
+ client))
+
+ @staticmethod
+ def get_gcp_resource_from_methodId(methodId):
+ try:
+ parts = methodId.split('.')
+ if len(parts) != 3:
+ return None
+ else:
+ return parts[1]
+ except AttributeError:
+ return None
+
+ @staticmethod
+ def get_entity_name_from_resource_name(resource_name):
+ if not resource_name:
+ return None
+
+ try:
+ # Chop off global or region prefixes
+ if resource_name.startswith('global'):
+ resource_name = resource_name.replace('global', '')
+ elif resource_name.startswith('regional'):
+ resource_name = resource_name.replace('region', '')
+
+ # ensure we have a lower case first letter
+ resource_name = resource_name[0].lower() + resource_name[1:]
+
+ if resource_name[-3:] == 'ies':
+ return resource_name.replace(
+ resource_name[-3:], 'y')
+ if resource_name[-1] == 's':
+ return resource_name[:-1]
+
+ return resource_name
+
+ except AttributeError:
+ return None
+
+ @staticmethod
+ def parse_gcp_url(url):
+ """
+ Parse GCP urls and return dict of parts.
+
+ Supported URL structures:
+ /SERVICE/VERSION/'projects'/PROJECT_ID/RESOURCE
+ /SERVICE/VERSION/'projects'/PROJECT_ID/RESOURCE/ENTITY_NAME
+ /SERVICE/VERSION/'projects'/PROJECT_ID/RESOURCE/ENTITY_NAME/METHOD_NAME
+ /SERVICE/VERSION/'projects'/PROJECT_ID/'global'/RESOURCE
+ /SERVICE/VERSION/'projects'/PROJECT_ID/'global'/RESOURCE/ENTITY_NAME
+ /SERVICE/VERSION/'projects'/PROJECT_ID/'global'/RESOURCE/ENTITY_NAME/METHOD_NAME
+ /SERVICE/VERSION/'projects'/PROJECT_ID/LOCATION_TYPE/LOCATION/RESOURCE
+ /SERVICE/VERSION/'projects'/PROJECT_ID/LOCATION_TYPE/LOCATION/RESOURCE/ENTITY_NAME
+ /SERVICE/VERSION/'projects'/PROJECT_ID/LOCATION_TYPE/LOCATION/RESOURCE/ENTITY_NAME/METHOD_NAME
+
+ :param url: GCP-generated URL, such as a selflink or resource location.
+ :type url: ``str``
+
+ :return: dictionary of parts. Includes stanard components of urlparse, plus
+ GCP-specific 'service', 'api_version', 'project' and
+ 'resource_name' keys. Optionally, 'zone', 'region', 'entity_name'
+ and 'method_name', if applicable.
+ :rtype: ``dict``
+ """
+
+ p = urlparse.urlparse(url)
+ if not p:
+ return None
+ else:
+ # we add extra items such as
+ # zone, region and resource_name
+ url_parts = {}
+ url_parts['scheme'] = p.scheme
+ url_parts['host'] = p.netloc
+ url_parts['path'] = p.path
+ if p.path.find('/') == 0:
+ url_parts['path'] = p.path[1:]
+ url_parts['params'] = p.params
+ url_parts['fragment'] = p.fragment
+ url_parts['query'] = p.query
+ url_parts['project'] = None
+ url_parts['service'] = None
+ url_parts['api_version'] = None
+
+ path_parts = url_parts['path'].split('/')
+ url_parts['service'] = path_parts[0]
+ url_parts['api_version'] = path_parts[1]
+ if path_parts[2] == 'projects':
+ url_parts['project'] = path_parts[3]
+ else:
+ # invalid URL
+ raise GCPInvalidURLError('unable to parse: %s' % url)
+
+ if 'global' in path_parts:
+ url_parts['global'] = True
+ idx = path_parts.index('global')
+ if len(path_parts) - idx == 4:
+ # we have a resource, entity and method_name
+ url_parts['resource_name'] = path_parts[idx + 1]
+ url_parts['entity_name'] = path_parts[idx + 2]
+ url_parts['method_name'] = path_parts[idx + 3]
+
+ if len(path_parts) - idx == 3:
+ # we have a resource and entity
+ url_parts['resource_name'] = path_parts[idx + 1]
+ url_parts['entity_name'] = path_parts[idx + 2]
+
+ if len(path_parts) - idx == 2:
+ url_parts['resource_name'] = path_parts[idx + 1]
+
+ if len(path_parts) - idx < 2:
+ # invalid URL
+ raise GCPInvalidURLError('unable to parse: %s' % url)
+
+ elif 'regions' in path_parts or 'zones' in path_parts:
+ idx = -1
+ if 'regions' in path_parts:
+ idx = path_parts.index('regions')
+ url_parts['region'] = path_parts[idx + 1]
+ else:
+ idx = path_parts.index('zones')
+ url_parts['zone'] = path_parts[idx + 1]
+
+ if len(path_parts) - idx == 5:
+ # we have a resource, entity and method_name
+ url_parts['resource_name'] = path_parts[idx + 2]
+ url_parts['entity_name'] = path_parts[idx + 3]
+ url_parts['method_name'] = path_parts[idx + 4]
+
+ if len(path_parts) - idx == 4:
+ # we have a resource and entity
+ url_parts['resource_name'] = path_parts[idx + 2]
+ url_parts['entity_name'] = path_parts[idx + 3]
+
+ if len(path_parts) - idx == 3:
+ url_parts['resource_name'] = path_parts[idx + 2]
+
+ if len(path_parts) - idx < 3:
+ # invalid URL
+ raise GCPInvalidURLError('unable to parse: %s' % url)
+
+ else:
+ # no location in URL.
+ idx = path_parts.index('projects')
+ if len(path_parts) - idx == 5:
+ # we have a resource, entity and method_name
+ url_parts['resource_name'] = path_parts[idx + 2]
+ url_parts['entity_name'] = path_parts[idx + 3]
+ url_parts['method_name'] = path_parts[idx + 4]
+
+ if len(path_parts) - idx == 4:
+ # we have a resource and entity
+ url_parts['resource_name'] = path_parts[idx + 2]
+ url_parts['entity_name'] = path_parts[idx + 3]
+
+ if len(path_parts) - idx == 3:
+ url_parts['resource_name'] = path_parts[idx + 2]
+
+ if len(path_parts) - idx < 3:
+ # invalid URL
+ raise GCPInvalidURLError('unable to parse: %s' % url)
+
+ return url_parts
+
+ @staticmethod
+ def build_googleapi_url(project, api_version='v1', service='compute'):
+ return 'https://www.googleapis.com/%s/%s/projects/%s' % (service, api_version, project)
+
+ @staticmethod
+ def filter_gcp_fields(params, excluded_fields=None):
+ new_params = {}
+ if not excluded_fields:
+ excluded_fields = ['creationTimestamp', 'id', 'kind',
+ 'selfLink', 'fingerprint', 'description']
+
+ if isinstance(params, list):
+ new_params = [GCPUtils.filter_gcp_fields(
+ x, excluded_fields) for x in params]
+ elif isinstance(params, dict):
+ for k in params.keys():
+ if k not in excluded_fields:
+ new_params[k] = GCPUtils.filter_gcp_fields(
+ params[k], excluded_fields)
+ else:
+ new_params = params
+
+ return new_params
+
+ @staticmethod
+ def are_params_equal(p1, p2):
+ """
+ Check if two params dicts are equal.
+ TODO(supertom): need a way to filter out URLs, or they need to be built
+ """
+ filtered_p1 = GCPUtils.filter_gcp_fields(p1)
+ filtered_p2 = GCPUtils.filter_gcp_fields(p2)
+ if filtered_p1 != filtered_p2:
+ return False
+ return True
+
+
+class GCPError(Exception):
+ pass
+
+
+class GCPOperationTimeoutError(GCPError):
+ pass
+
+
+class GCPInvalidURLError(GCPError):
+ pass
diff --git a/lib/ansible/modules/cloud/google/gcp_url_map.py b/lib/ansible/modules/cloud/google/gcp_url_map.py
new file mode 100644
index 0000000000..e865327f64
--- /dev/null
+++ b/lib/ansible/modules/cloud/google/gcp_url_map.py
@@ -0,0 +1,519 @@
+#!/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_url_map
+version_added: "2.4"
+short_description: Create, Update or Destory a Url_Map.
+description:
+ - Create, Update or Destory a Url_Map. See
+ U(https://cloud.google.com/compute/docs/load-balancing/http/url-map) for an overview.
+ More details on the Url_Map API can be found at
+ U(https://cloud.google.com/compute/docs/reference/latest/urlMaps#resource).
+requirements:
+ - "python >= 2.6"
+ - "google-api-python-client >= 1.6.2"
+ - "google-auth >= 0.9.0"
+ - "google-auth-httplib2 >= 0.0.2"
+notes:
+ - Only supports global Backend Services.
+ - Url_Map tests are not currently supported.
+author:
+ - "Tom Melendez (@supertom) "
+options:
+ url_map_name:
+ description:
+ - Name of the Url_Map.
+ required: true
+ default_service:
+ description:
+ - Default Backend Service if no host rules match.
+ required: true
+ host_rules:
+ description:
+ - The list of HostRules to use against the URL. Contains
+ a list of hosts and an associated path_matcher.
+ - The 'hosts' parameter is a list of host patterns to match. They
+ must be valid hostnames, except * will match any string of
+ ([a-z0-9-.]*). In that case, * must be the first character
+ and must be followed in the pattern by either - or ..
+ - The 'path_matcher' parameter is name of the PathMatcher to use
+ to match the path portion of the URL if the hostRule matches the URL's
+ host portion.
+ required: false
+ path_matchers:
+ description:
+ - The list of named PathMatchers to use against the URL. Contains
+ path_rules, which is a list of paths and an associated service. A
+ default_service can also be specified for each path_matcher.
+ - The 'name' parameter to which this path_matcher is referred by the
+ host_rule.
+ - The 'default_service' parameter is the name of the
+ BackendService resource. This will be used if none of the path_rules
+ defined by this path_matcher is matched by the URL's path portion.
+ - The 'path_rules' parameter is a list of dictionaries containing a
+ list of paths and a service to direct traffic to. Each path item must
+ start with / and the only place a * is allowed is at the end following
+ a /. The string fed to the path matcher does not include any text after
+ the first ? or #, and those chars are not allowed here.
+ required: false
+'''
+
+EXAMPLES = '''
+- name: Create Minimal Url_Map
+ gcp_url_map:
+ service_account_email: "{{ service_account_email }}"
+ credentials_file: "{{ credentials_file }}"
+ project_id: "{{ project_id }}"
+ url_map_name: my-url_map
+ default_service: my-backend-service
+ state: present
+- name: Create UrlMap with pathmatcher
+ gcp_url_map:
+ service_account_email: "{{ service_account_email }}"
+ credentials_file: "{{ credentials_file }}"
+ project_id: "{{ project_id }}"
+ url_map_name: my-url-map-pm
+ default_service: default-backend-service
+ path_matchers:
+ - name: 'path-matcher-one'
+ description: 'path matcher one'
+ default_service: 'bes-pathmatcher-one-default'
+ path_rules:
+ - service: 'my-one-bes'
+ paths:
+ - '/data'
+ - '/aboutus'
+ host_rules:
+ - hosts:
+ - '*.'
+ path_matcher: 'path-matcher-one'
+ state: "present"
+'''
+
+RETURN = '''
+host_rules:
+ description: List of HostRules.
+ returned: If specified.
+ type: dict
+ sample: [ { hosts: ["*."], "path_matcher": "my-pm" } ]
+path_matchers:
+ description: The list of named PathMatchers to use against the URL.
+ returned: If specified.
+ type: dict
+ sample: [ { "name": "my-pm", "path_rules": [ { "paths": [ "/data" ] } ], "service": "my-service" } ]
+state:
+ description: state of the Url_Map
+ returned: Always.
+ type: str
+ sample: present
+updated_url_map:
+ description: True if the url_map has been updated. Will not appear on
+ initial url_map creation.
+ returned: if the url_map has been updated.
+ type: bool
+ sample: true
+url_map_name:
+ description: Name of the Url_Map
+ returned: Always
+ type: str
+ sample: my-url-map
+url_map:
+ description: GCP Url_Map dictionary
+ returned: Always. Refer to GCP documentation for detailed field descriptions.
+ type: dict
+ sample: { "name": "my-url-map", "hostRules": [...], "pathMatchers": [...] }
+'''
+
+
+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.gcp import check_params, get_google_api_client, GCPUtils
+
+USER_AGENT_PRODUCT = 'ansible-url_map'
+USER_AGENT_VERSION = '0.0.1'
+
+
+def _validate_params(params):
+ """
+ Validate url_map params.
+
+ This function calls _validate_host_rules_params to verify
+ the host_rules-specific parameters.
+
+ This function calls _validate_path_matchers_params to verify
+ the path_matchers-specific parameters.
+
+ :param params: Ansible dictionary containing configuration.
+ :type params: ``dict``
+
+ :return: True or raises ValueError
+ :rtype: ``bool`` or `class:ValueError`
+ """
+ fields = [
+ {'name': 'default_service', 'type': str, 'required': True},
+ {'name': 'host_rules', 'type': list},
+ {'name': 'path_matchers', 'type': list},
+ ]
+ try:
+ check_params(params, fields)
+ if 'path_matchers' in params and params['path_matchers'] is not None:
+ _validate_path_matcher_params(params['path_matchers'])
+ if 'host_rules' in params and params['host_rules'] is not None:
+ _validate_host_rules_params(params['host_rules'])
+ except:
+ raise
+
+ return (True, '')
+
+
+def _validate_path_matcher_params(path_matchers):
+ """
+ Validate configuration for path_matchers.
+
+ :param path_matchers: Ansible dictionary containing path_matchers
+ configuration (only).
+ :type path_matchers: ``dict``
+
+ :return: True or raises ValueError
+ :rtype: ``bool`` or `class:ValueError`
+ """
+ fields = [
+ {'name': 'name', 'type': str, 'required': True},
+ {'name': 'default_service', 'type': str, 'required': True},
+ {'name': 'path_rules', 'type': list, 'required': True},
+ {'name': 'max_rate', 'type': int},
+ {'name': 'max_rate_per_instance', 'type': float},
+ ]
+ pr_fields = [
+ {'name': 'service', 'type': str, 'required': True},
+ {'name': 'paths', 'type': list, 'required': True},
+ ]
+
+ if not path_matchers:
+ raise ValueError(('path_matchers should be a list. %s (%s) provided'
+ % (path_matchers, type(path_matchers))))
+
+ for pm in path_matchers:
+ try:
+ check_params(pm, fields)
+ for pr in pm['path_rules']:
+ check_params(pr, pr_fields)
+ for path in pr['paths']:
+ if not path.startswith('/'):
+ raise ValueError("path for %s must start with /" % (
+ pm['name']))
+ except:
+ raise
+
+ return (True, '')
+
+
+def _validate_host_rules_params(host_rules):
+ """
+ Validate configuration for host_rules.
+
+ :param host_rules: Ansible dictionary containing host_rules
+ configuration (only).
+ :type host_rules ``dict``
+
+ :return: True or raises ValueError
+ :rtype: ``bool`` or `class:ValueError`
+ """
+ fields = [
+ {'name': 'path_matcher', 'type': str, 'required': True},
+ ]
+
+ if not host_rules:
+ raise ValueError('host_rules should be a list.')
+
+ for hr in host_rules:
+ try:
+ check_params(hr, fields)
+ for host in hr['hosts']:
+ if not isinstance(host, basestring):
+ raise ValueError("host in hostrules must be a string")
+ elif '*' in host:
+ if host.index('*') != 0:
+ raise ValueError("wildcard must be first char in host, %s" % (
+ host))
+ else:
+ if host[1] not in ['.', '-', ]:
+ raise ValueError("wildcard be followed by a '.' or '-', %s" % (
+ host))
+
+ except:
+ raise
+
+ return (True, '')
+
+
+def _build_path_matchers(path_matcher_list, project_id):
+ """
+ Reformat services in path matchers list.
+
+ Specifically, builds out URLs.
+
+ :param path_matcher_list: The GCP project ID.
+ :type path_matcher_list: ``list`` of ``dict``
+
+ :param project_id: The GCP project ID.
+ :type project_id: ``str``
+
+ :return: list suitable for submission to GCP
+ UrlMap API Path Matchers list.
+ :rtype ``list`` of ``dict``
+ """
+ url = ''
+ if project_id:
+ url = GCPUtils.build_googleapi_url(project_id)
+ for pm in path_matcher_list:
+ if 'defaultService' in pm:
+ pm['defaultService'] = '%s/global/backendServices/%s' % (url,
+ pm['defaultService'])
+ if 'pathRules' in pm:
+ for rule in pm['pathRules']:
+ if 'service' in rule:
+ rule['service'] = '%s/global/backendServices/%s' % (url,
+ rule['service'])
+ return path_matcher_list
+
+
+def _build_url_map_dict(params, project_id=None):
+ """
+ Reformat services in Ansible Params.
+
+ :param params: Params from AnsibleModule object
+ :type params: ``dict``
+
+ :param project_id: The GCP project ID.
+ :type project_id: ``str``
+
+ :return: dictionary suitable for submission to GCP UrlMap API.
+ :rtype ``dict``
+ """
+ url = ''
+ if project_id:
+ url = GCPUtils.build_googleapi_url(project_id)
+ gcp_dict = GCPUtils.params_to_gcp_dict(params, 'url_map_name')
+ if 'defaultService' in gcp_dict:
+ gcp_dict['defaultService'] = '%s/global/backendServices/%s' % (url,
+ gcp_dict['defaultService'])
+ if 'pathMatchers' in gcp_dict:
+ gcp_dict['pathMatchers'] = _build_path_matchers(gcp_dict['pathMatchers'], project_id)
+
+ return gcp_dict
+
+
+def get_url_map(client, name, project_id=None):
+ """
+ Get a Url_Map from GCP.
+
+ :param client: An initialized GCE Compute Disovery resource.
+ :type client: :class: `googleapiclient.discovery.Resource`
+
+ :param name: Name of the Url Map.
+ :type name: ``str``
+
+ :param project_id: The GCP project ID.
+ :type project_id: ``str``
+
+ :return: A dict resp from the respective GCP 'get' request.
+ :rtype: ``dict``
+ """
+ try:
+ req = client.urlMaps().get(project=project_id, urlMap=name)
+ return GCPUtils.execute_api_client_req(req, raise_404=False)
+ except:
+ raise
+
+
+def create_url_map(client, params, project_id):
+ """
+ Create a new Url_Map.
+
+ :param client: An initialized GCE Compute Disovery resource.
+ :type client: :class: `googleapiclient.discovery.Resource`
+
+ :param params: Dictionary of arguments from AnsibleModule.
+ :type params: ``dict``
+
+ :return: Tuple with changed status and response dict
+ :rtype: ``tuple`` in the format of (bool, dict)
+ """
+ gcp_dict = _build_url_map_dict(params, project_id)
+ try:
+ req = client.urlMaps().insert(project=project_id, body=gcp_dict)
+ return_data = GCPUtils.execute_api_client_req(req, client, raw=False)
+ if not return_data:
+ return_data = get_url_map(client,
+ name=params['url_map_name'],
+ project_id=project_id)
+ return (True, return_data)
+ except:
+ raise
+
+
+def delete_url_map(client, name, project_id):
+ """
+ Delete a Url_Map.
+
+ :param client: An initialized GCE Compute Disover resource.
+ :type client: :class: `googleapiclient.discovery.Resource`
+
+ :param name: Name of the Url Map.
+ :type name: ``str``
+
+ :param project_id: The GCP project ID.
+ :type project_id: ``str``
+
+ :return: Tuple with changed status and response dict
+ :rtype: ``tuple`` in the format of (bool, dict)
+ """
+ try:
+ req = client.urlMaps().delete(project=project_id, urlMap=name)
+ return_data = GCPUtils.execute_api_client_req(req, client)
+ return (True, return_data)
+ except:
+ raise
+
+
+def update_url_map(client, url_map, params, name, project_id):
+ """
+ Update a Url_Map.
+
+ If the url_map has not changed, the update will not occur.
+
+ :param client: An initialized GCE Compute Disovery resource.
+ :type client: :class: `googleapiclient.discovery.Resource`
+
+ :param url_map: Name of the Url Map.
+ :type url_map: ``dict``
+
+ :param params: Dictionary of arguments from AnsibleModule.
+ :type params: ``dict``
+
+ :param name: Name of the Url Map.
+ :type name: ``str``
+
+ :param project_id: The GCP project ID.
+ :type project_id: ``str``
+
+ :return: Tuple with changed status and response dict
+ :rtype: ``tuple`` in the format of (bool, dict)
+ """
+ gcp_dict = _build_url_map_dict(params, project_id)
+
+ ans = GCPUtils.are_params_equal(url_map, gcp_dict)
+ if ans:
+ return (False, 'no update necessary')
+
+ gcp_dict['fingerprint'] = url_map['fingerprint']
+ try:
+ req = client.urlMaps().update(project=project_id,
+ urlMap=name, body=gcp_dict)
+ return_data = GCPUtils.execute_api_client_req(req, client=client, raw=False)
+ return (True, return_data)
+ except:
+ raise
+
+
+def main():
+ module = AnsibleModule(argument_spec=dict(
+ url_map_name=dict(required=True),
+ state=dict(choices=['absent', 'present'], default='present'),
+ default_service=dict(required=True),
+ path_matchers=dict(type='list', required=False),
+ host_rules=dict(type='list', required=False),
+ service_account_email=dict(),
+ service_account_permissions=dict(type='list'),
+ pem_file=dict(),
+ credentials_file=dict(),
+ project_id=dict(), ), required_together=[
+ ['path_matchers', 'host_rules'], ])
+
+ if not HAS_PYTHON26:
+ module.fail_json(
+ msg="GCE module requires python's 'ast' module, python v2.6+")
+
+ client, conn_params = get_google_api_client(module, 'compute', user_agent_product=USER_AGENT_PRODUCT,
+ user_agent_version=USER_AGENT_VERSION)
+
+ params = {}
+ params['state'] = module.params.get('state')
+ params['url_map_name'] = module.params.get('url_map_name')
+ params['default_service'] = module.params.get('default_service')
+ if module.params.get('path_matchers'):
+ params['path_matchers'] = module.params.get('path_matchers')
+ if module.params.get('host_rules'):
+ params['host_rules'] = module.params.get('host_rules')
+
+ try:
+ _validate_params(params)
+ except Exception as e:
+ module.fail_json(msg=e.message, changed=False)
+
+ changed = False
+ json_output = {'state': params['state']}
+ url_map = get_url_map(client,
+ name=params['url_map_name'],
+ project_id=conn_params['project_id'])
+
+ if not url_map:
+ if params['state'] == 'absent':
+ # Doesn't exist in GCE, and state==absent.
+ changed = False
+ module.fail_json(
+ msg="Cannot delete unknown url_map: %s" %
+ (params['url_map_name']))
+ else:
+ # Create
+ changed, json_output['url_map'] = create_url_map(client,
+ params=params,
+ project_id=conn_params['project_id'])
+ elif params['state'] == 'absent':
+ # Delete
+ changed, json_output['url_map'] = delete_url_map(client,
+ name=params['url_map_name'],
+ project_id=conn_params['project_id'])
+ else:
+ changed, json_output['url_map'] = update_url_map(client,
+ url_map=url_map,
+ params=params,
+ name=params['url_map_name'],
+ project_id=conn_params['project_id'])
+ json_output['updated_url_map'] = changed
+
+ json_output['changed'] = changed
+ json_output.update(params)
+ module.exit_json(**json_output)
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/gce.yml b/test/integration/gce.yml
index 1c7dafb6a1..268b025770 100644
--- a/test/integration/gce.yml
+++ b/test/integration/gce.yml
@@ -7,4 +7,5 @@
- { role: test_gcdns, tags: test_gcdns }
- { role: test_gce_tag, tags: test_gce_tag }
- { role: test_gce_net, tags: test_gce_net }
+ - { role: test_gcp_url_map, tags: test_gcp_url_map }
# TODO: tests for gce_lb, gc_storage
diff --git a/test/integration/roles/test_gcp_url_map/defaults/main.yml b/test/integration/roles/test_gcp_url_map/defaults/main.yml
new file mode 100644
index 0000000000..ce48baab5d
--- /dev/null
+++ b/test/integration/roles/test_gcp_url_map/defaults/main.yml
@@ -0,0 +1,6 @@
+---
+# defaults file for test_gcp_url_map
+service_account_email: "{{ gce_service_account_email }}"
+credentials_file: "{{ gce_pem_file }}"
+project_id: "{{ gce_project_id }}"
+urlmap: "ans-int-urlmap-{{ resource_prefix|lower }}"
\ No newline at end of file
diff --git a/test/integration/roles/test_gcp_url_map/tasks/main.yml b/test/integration/roles/test_gcp_url_map/tasks/main.yml
new file mode 100644
index 0000000000..53c31dc39d
--- /dev/null
+++ b/test/integration/roles/test_gcp_url_map/tasks/main.yml
@@ -0,0 +1,178 @@
+# GCP UrlMap Integration Tests.
+# Only parameter tests are currently done in this file as this module requires
+# a significant amount of infrastructure.
+######
+# ============================================================
+- name: "Create UrlMap with no default service (changed == False)"
+# ============================================================
+ gcp_url_map:
+ service_account_email: "{{ service_account_email }}"
+ credentials_file: "{{ credentials_file }}"
+ project_id: "{{ project_id }}"
+ url_map_name: "{{ urlmap }}"
+ host_rules:
+ - hosts:
+ - '*.'
+ path_matcher: 'path-matcher-one'
+ state: "present"
+ register: result
+ ignore_errors: True
+ tags:
+ - param-check
+- name: "assert urlmap no default service (msg error ignored, changed==False)"
+ assert:
+ that:
+ - 'not result.changed'
+ - 'result.msg == "missing required arguments: default_service"'
+
+# ============================================================
+- name: "Create UrlMap with no pathmatcher (changed == False)"
+# ============================================================
+ gcp_url_map:
+ service_account_email: "{{ service_account_email }}"
+ credentials_file: "{{ credentials_file }}"
+ project_id: "{{ project_id }}"
+ url_map_name: "{{ urlmap }}"
+ default_service: "gfr2-bes"
+ host_rules:
+ - hosts:
+ - '*.'
+ path_matcher: 'path-matcher-one'
+ state: "present"
+ register: result
+ ignore_errors: True
+ tags:
+ - param-check
+- name: "assert urlmap no path_matcher (msg error ignored, changed==False)"
+ assert:
+ that:
+ - 'not result.changed'
+ - 'result.msg == "parameters are required together: [''path_matchers'', ''host_rules'']"'
+
+# ============================================================
+- name: "Create UrlMap with no hostrules (changed == False)"
+# ============================================================
+ gcp_url_map:
+ service_account_email: "{{ service_account_email }}"
+ credentials_file: "{{ credentials_file }}"
+ project_id: "{{ project_id }}"
+ url_map_name: "{{ urlmap }}"
+ default_service: "gfr2-bes"
+ path_matchers:
+ - name: 'path-matcher-one'
+ description: 'path matcher one'
+ default_service: 'gfr-bes'
+ path_rules:
+ - service: 'gfr2-bes'
+ paths:
+ - '/data'
+ - '/aboutus'
+ state: "present"
+ tags:
+ - param-check
+ register: result
+ ignore_errors: True
+- name: "assert no host_rules (msg error ignored, changed==False)"
+ assert:
+ that:
+ - 'not result.changed'
+ - 'result.msg == "parameters are required together: [''path_matchers'', ''host_rules'']"'
+
+# ============================================================
+- name: "Update UrlMap with non-absolute paths (changed==False)"
+# ============================================================
+ gcp_url_map:
+ service_account_email: "{{ service_account_email }}"
+ credentials_file: "{{ credentials_file }}"
+ project_id: "{{ project_id }}"
+ url_map_name: "{{ urlmap }}"
+ default_service: "gfr2-bes"
+ path_matchers:
+ - name: 'path-matcher-one'
+ description: 'path matcher one'
+ default_service: 'gfr-bes'
+ path_rules:
+ - service: 'gfr2-bes'
+ paths:
+ - 'data'
+ - 'aboutus'
+ host_rules:
+ - hosts:
+ - '*.'
+ path_matcher: 'path-matcher-one'
+ state: "present"
+ tags:
+ - param-check
+ ignore_errors: True
+ register: result
+- name: "assert path error updated (changed==False)"
+ assert:
+ that:
+ - 'not result.changed'
+ - 'result.msg == "path for path-matcher-one must start with /"'
+
+# ============================================================
+- name: "Update UrlMap with invalid wildcard host (changed==False)"
+# ============================================================
+ gcp_url_map:
+ service_account_email: "{{ service_account_email }}"
+ credentials_file: "{{ credentials_file }}"
+ project_id: "{{ project_id }}"
+ url_map_name: "{{ urlmap }}"
+ default_service: "gfr2-bes"
+ path_matchers:
+ - name: 'path-matcher-one'
+ description: 'path matcher one'
+ default_service: 'gfr-bes'
+ path_rules:
+ - service: 'gfr2-bes'
+ paths:
+ - '/data'
+ - '/aboutus'
+ host_rules:
+ - hosts:
+ - 'foobar*'
+ path_matcher: 'path-matcher-one'
+ state: "present"
+ tags:
+ - param-check
+ ignore_errors: True
+ register: result
+- name: "assert host wildcard error (error msg ignored, changed==False)"
+ assert:
+ that:
+ - 'not result.changed'
+ - 'result.msg == "wildcard must be first char in host, foobar*"'
+
+# ============================================================
+- name: "Update UrlMap with invalid wildcard host second char (changed==False)"
+# ============================================================
+ gcp_url_map:
+ service_account_email: "{{ service_account_email }}"
+ credentials_file: "{{ credentials_file }}"
+ project_id: "{{ project_id }}"
+ url_map_name: "{{ urlmap }}"
+ default_service: "gfr2-bes"
+ path_matchers:
+ - name: 'path-matcher-one'
+ description: 'path matcher one'
+ default_service: 'gfr-bes'
+ path_rules:
+ - service: 'gfr2-bes'
+ paths:
+ - '/data'
+ - '/aboutus'
+ host_rules:
+ - hosts:
+ - '*='
+ path_matcher: 'path-matcher-one'
+ state: "present"
+ tags:
+ - param-check
+ ignore_errors: True
+ register: result
+- name: "assert wildcard error second char (error msg ignored, changed==False)"
+ assert:
+ that:
+ - 'not result.changed'
+ - 'result.msg == "wildcard be followed by a ''.'' or ''-'', *="'
diff --git a/test/sanity/pep8/legacy-files.txt b/test/sanity/pep8/legacy-files.txt
index bd250c4178..897c0f7d59 100644
--- a/test/sanity/pep8/legacy-files.txt
+++ b/test/sanity/pep8/legacy-files.txt
@@ -913,7 +913,6 @@ test/units/module_utils/basic/test_safe_eval.py
test/units/module_utils/basic/test_set_mode_if_different.py
test/units/module_utils/ec2/test_aws.py
test/units/module_utils/gcp/test_auth.py
-test/units/module_utils/gcp/test_utils.py
test/units/module_utils/json_utils/test_filter_non_json_lines.py
test/units/module_utils/test_basic.py
test/units/module_utils/test_distribution_version.py
diff --git a/test/units/module_utils/gcp/test_utils.py b/test/units/module_utils/gcp/test_utils.py
index 75795c9e1c..9842521949 100644
--- a/test/units/module_utils/gcp/test_utils.py
+++ b/test/units/module_utils/gcp/test_utils.py
@@ -19,7 +19,8 @@ import os
import sys
from ansible.compat.tests import mock, unittest
-from ansible.module_utils.gcp import (check_min_pkg_version)
+from ansible.module_utils.gcp import check_min_pkg_version, GCPUtils, GCPInvalidURLError
+
def build_distribution(version):
obj = mock.MagicMock()
@@ -28,9 +29,316 @@ def build_distribution(version):
class GCPUtilsTestCase(unittest.TestCase):
+ params_dict = {
+ 'url_map_name': 'foo_url_map_name',
+ 'description': 'foo_url_map description',
+ 'host_rules': [
+ {
+ 'description': 'host rules description',
+ 'hosts': [
+ 'www.example.com',
+ 'www2.example.com'
+ ],
+ 'path_matcher': 'host_rules_path_matcher'
+ }
+ ],
+ 'path_matchers': [
+ {
+ 'name': 'path_matcher_one',
+ 'description': 'path matcher one',
+ 'defaultService': 'bes-pathmatcher-one-default',
+ 'pathRules': [
+ {
+ 'service': 'my-one-bes',
+ 'paths': [
+ '/',
+ '/aboutus'
+ ]
+ }
+ ]
+ },
+ {
+ 'name': 'path_matcher_two',
+ 'description': 'path matcher two',
+ 'defaultService': 'bes-pathmatcher-two-default',
+ 'pathRules': [
+ {
+ 'service': 'my-two-bes',
+ 'paths': [
+ '/webapp',
+ '/graphs'
+ ]
+ }
+ ]
+ }
+ ]
+ }
@mock.patch("pkg_resources.get_distribution", side_effect=build_distribution)
def test_check_minimum_pkg_version(self, mockobj):
self.assertTrue(check_min_pkg_version('foobar', '0.4.0'))
self.assertTrue(check_min_pkg_version('foobar', '0.5.0'))
self.assertFalse(check_min_pkg_version('foobar', '0.6.0'))
+
+ def test_parse_gcp_url(self):
+ # region, resource, entity, method
+ input_url = 'https://www.googleapis.com/compute/v1/projects/myproject/regions/us-east1/instanceGroupManagers/my-mig/recreateInstances'
+ actual = GCPUtils.parse_gcp_url(input_url)
+ self.assertEquals('compute', actual['service'])
+ self.assertEquals('v1', actual['api_version'])
+ self.assertEquals('myproject', actual['project'])
+ self.assertEquals('us-east1', actual['region'])
+ self.assertEquals('instanceGroupManagers', actual['resource_name'])
+ self.assertEquals('my-mig', actual['entity_name'])
+ self.assertEquals('recreateInstances', actual['method_name'])
+
+ # zone, resource, entity, method
+ input_url = 'https://www.googleapis.com/compute/v1/projects/myproject/zones/us-east1-c/instanceGroupManagers/my-mig/recreateInstances'
+ actual = GCPUtils.parse_gcp_url(input_url)
+ self.assertEquals('compute', actual['service'])
+ self.assertEquals('v1', actual['api_version'])
+ self.assertEquals('myproject', actual['project'])
+ self.assertEquals('us-east1-c', actual['zone'])
+ self.assertEquals('instanceGroupManagers', actual['resource_name'])
+ self.assertEquals('my-mig', actual['entity_name'])
+ self.assertEquals('recreateInstances', actual['method_name'])
+
+ # global, resource
+ input_url = 'https://www.googleapis.com/compute/v1/projects/myproject/global/urlMaps'
+ actual = GCPUtils.parse_gcp_url(input_url)
+ self.assertEquals('compute', actual['service'])
+ self.assertEquals('v1', actual['api_version'])
+ self.assertEquals('myproject', actual['project'])
+ self.assertTrue('global' in actual)
+ self.assertTrue(actual['global'])
+ self.assertEquals('urlMaps', actual['resource_name'])
+
+ # global, resource, entity
+ input_url = 'https://www.googleapis.com/compute/v1/projects/myproject/global/urlMaps/my-url-map'
+ actual = GCPUtils.parse_gcp_url(input_url)
+ self.assertEquals('myproject', actual['project'])
+ self.assertTrue('global' in actual)
+ self.assertTrue(actual['global'])
+ self.assertEquals('v1', actual['api_version'])
+ self.assertEquals('compute', actual['service'])
+
+ # global URL, resource, entity, method_name
+ input_url = 'https://www.googleapis.com/compute/v1/projects/myproject/global/backendServices/mybackendservice/getHealth'
+ actual = GCPUtils.parse_gcp_url(input_url)
+ self.assertEquals('compute', actual['service'])
+ self.assertEquals('v1', actual['api_version'])
+ self.assertEquals('myproject', actual['project'])
+ self.assertTrue('global' in actual)
+ self.assertTrue(actual['global'])
+ self.assertEquals('backendServices', actual['resource_name'])
+ self.assertEquals('mybackendservice', actual['entity_name'])
+ self.assertEquals('getHealth', actual['method_name'])
+
+ # no location in URL
+ input_url = 'https://www.googleapis.com/compute/v1/projects/myproject/targetHttpProxies/mytargetproxy/setUrlMap'
+ actual = GCPUtils.parse_gcp_url(input_url)
+ self.assertEquals('compute', actual['service'])
+ self.assertEquals('v1', actual['api_version'])
+ self.assertEquals('myproject', actual['project'])
+ self.assertFalse('global' in actual)
+ self.assertEquals('targetHttpProxies', actual['resource_name'])
+ self.assertEquals('mytargetproxy', actual['entity_name'])
+ self.assertEquals('setUrlMap', actual['method_name'])
+
+ input_url = 'https://www.googleapis.com/compute/v1/projects/myproject/targetHttpProxies/mytargetproxy'
+ actual = GCPUtils.parse_gcp_url(input_url)
+ self.assertEquals('compute', actual['service'])
+ self.assertEquals('v1', actual['api_version'])
+ self.assertEquals('myproject', actual['project'])
+ self.assertFalse('global' in actual)
+ self.assertEquals('targetHttpProxies', actual['resource_name'])
+ self.assertEquals('mytargetproxy', actual['entity_name'])
+
+ input_url = 'https://www.googleapis.com/compute/v1/projects/myproject/targetHttpProxies'
+ actual = GCPUtils.parse_gcp_url(input_url)
+ self.assertEquals('compute', actual['service'])
+ self.assertEquals('v1', actual['api_version'])
+ self.assertEquals('myproject', actual['project'])
+ self.assertFalse('global' in actual)
+ self.assertEquals('targetHttpProxies', actual['resource_name'])
+
+ # test exceptions
+ no_projects_input_url = 'https://www.googleapis.com/compute/v1/not-projects/myproject/global/backendServices/mybackendservice/getHealth'
+ no_resource_input_url = 'https://www.googleapis.com/compute/v1/not-projects/myproject/global'
+
+ no_resource_no_loc_input_url = 'https://www.googleapis.com/compute/v1/not-projects/myproject'
+
+ with self.assertRaises(GCPInvalidURLError) as cm:
+ GCPUtils.parse_gcp_url(no_projects_input_url)
+ self.assertTrue(cm.exception, GCPInvalidURLError)
+
+ with self.assertRaises(GCPInvalidURLError) as cm:
+ GCPUtils.parse_gcp_url(no_resource_input_url)
+ self.assertTrue(cm.exception, GCPInvalidURLError)
+
+ with self.assertRaises(GCPInvalidURLError) as cm:
+ GCPUtils.parse_gcp_url(no_resource_no_loc_input_url)
+ self.assertTrue(cm.exception, GCPInvalidURLError)
+
+ def test_params_to_gcp_dict(self):
+
+ expected = {
+ 'description': 'foo_url_map description',
+ 'hostRules': [
+ {
+ 'description': 'host rules description',
+ 'hosts': [
+ 'www.example.com',
+ 'www2.example.com'
+ ],
+ 'pathMatcher': 'host_rules_path_matcher'
+ }
+ ],
+ 'name': 'foo_url_map_name',
+ 'pathMatchers': [
+ {
+ 'defaultService': 'bes-pathmatcher-one-default',
+ 'description': 'path matcher one',
+ 'name': 'path_matcher_one',
+ 'pathRules': [
+ {
+ 'paths': [
+ '/',
+ '/aboutus'
+ ],
+ 'service': 'my-one-bes'
+ }
+ ]
+ },
+ {
+ 'defaultService': 'bes-pathmatcher-two-default',
+ 'description': 'path matcher two',
+ 'name': 'path_matcher_two',
+ 'pathRules': [
+ {
+ 'paths': [
+ '/webapp',
+ '/graphs'
+ ],
+ 'service': 'my-two-bes'
+ }
+ ]
+ }
+ ]
+ }
+
+ actual = GCPUtils.params_to_gcp_dict(self.params_dict, 'url_map_name')
+ self.assertEqual(expected, actual)
+
+ def test_get_gcp_resource_from_methodId(self):
+ input_data = 'compute.urlMaps.list'
+ actual = GCPUtils.get_gcp_resource_from_methodId(input_data)
+ self.assertEqual('urlMaps', actual)
+ input_data = None
+ actual = GCPUtils.get_gcp_resource_from_methodId(input_data)
+ self.assertFalse(actual)
+ input_data = 666
+ actual = GCPUtils.get_gcp_resource_from_methodId(input_data)
+ self.assertFalse(actual)
+
+ def test_get_entity_name_from_resource_name(self):
+ input_data = 'urlMaps'
+ actual = GCPUtils.get_entity_name_from_resource_name(input_data)
+ self.assertEqual('urlMap', actual)
+ input_data = 'targetHttpProxies'
+ actual = GCPUtils.get_entity_name_from_resource_name(input_data)
+ self.assertEqual('targetHttpProxy', actual)
+ input_data = 'globalForwardingRules'
+ actual = GCPUtils.get_entity_name_from_resource_name(input_data)
+ self.assertEqual('forwardingRule', actual)
+ input_data = ''
+ actual = GCPUtils.get_entity_name_from_resource_name(input_data)
+ self.assertEqual(None, actual)
+ input_data = 666
+ actual = GCPUtils.get_entity_name_from_resource_name(input_data)
+ self.assertEqual(None, actual)
+
+ def test_are_params_equal(self):
+ params1 = {'one': 1}
+ params2 = {'one': 1}
+ actual = GCPUtils.are_params_equal(params1, params2)
+ self.assertTrue(actual)
+
+ params1 = {'one': 1}
+ params2 = {'two': 2}
+ actual = GCPUtils.are_params_equal(params1, params2)
+ self.assertFalse(actual)
+
+ params1 = {'three': 3, 'two': 2, 'one': 1}
+ params2 = {'one': 1, 'two': 2, 'three': 3}
+ actual = GCPUtils.are_params_equal(params1, params2)
+ self.assertTrue(actual)
+
+ params1 = {
+ "creationTimestamp": "2017-04-21T11:19:20.718-07:00",
+ "defaultService": "https://www.googleapis.com/compute/v1/projects/myproject/global/backendServices/default-backend-service",
+ "description": "",
+ "fingerprint": "ickr_pwlZPU=",
+ "hostRules": [
+ {
+ "description": "",
+ "hosts": [
+ "*."
+ ],
+ "pathMatcher": "path-matcher-one"
+ }
+ ],
+ "id": "8566395781175047111",
+ "kind": "compute#urlMap",
+ "name": "newtesturlmap-foo",
+ "pathMatchers": [
+ {
+ "defaultService": "https://www.googleapis.com/compute/v1/projects/myproject/global/backendServices/bes-pathmatcher-one-default",
+ "description": "path matcher one",
+ "name": "path-matcher-one",
+ "pathRules": [
+ {
+ "paths": [
+ "/data",
+ "/aboutus"
+ ],
+ "service": "https://www.googleapis.com/compute/v1/projects/myproject/global/backendServices/my-one-bes"
+ }
+ ]
+ }
+ ],
+ "selfLink": "https://www.googleapis.com/compute/v1/projects/myproject/global/urlMaps/newtesturlmap-foo"
+ }
+ params2 = {
+ "defaultService": "https://www.googleapis.com/compute/v1/projects/myproject/global/backendServices/default-backend-service",
+ "hostRules": [
+ {
+ "description": "",
+ "hosts": [
+ "*."
+ ],
+ "pathMatcher": "path-matcher-one"
+ }
+ ],
+ "name": "newtesturlmap-foo",
+ "pathMatchers": [
+ {
+ "defaultService": "https://www.googleapis.com/compute/v1/projects/myproject/global/backendServices/bes-pathmatcher-one-default",
+ "description": "path matcher one",
+ "name": "path-matcher-one",
+ "pathRules": [
+ {
+ "paths": [
+ "/data",
+ "/aboutus"
+ ],
+ "service": "https://www.googleapis.com/compute/v1/projects/myproject/global/backendServices/my-one-bes"
+ }
+ ]
+ }
+ ],
+ }
+
+ # params1 has exclude fields, params2 doesn't. Should be equal
+ actual = GCPUtils.are_params_equal(params1, params2)
+ self.assertTrue(actual)
diff --git a/test/units/modules/cloud/google/test_gcp_url_map.py b/test/units/modules/cloud/google/test_gcp_url_map.py
new file mode 100644
index 0000000000..6178c4cecb
--- /dev/null
+++ b/test/units/modules/cloud/google/test_gcp_url_map.py
@@ -0,0 +1,164 @@
+import unittest
+
+from ansible.modules.cloud.google.gcp_url_map import _build_path_matchers, _build_url_map_dict
+
+
+class TestGCPUrlMap(unittest.TestCase):
+ """Unit tests for gcp_url_map module."""
+ params_dict = {
+ 'url_map_name': 'foo_url_map_name',
+ 'description': 'foo_url_map description',
+ 'host_rules': [
+ {
+ 'description': 'host rules description',
+ 'hosts': [
+ 'www.example.com',
+ 'www2.example.com'
+ ],
+ 'path_matcher': 'host_rules_path_matcher'
+ }
+ ],
+ 'path_matchers': [
+ {
+ 'name': 'path_matcher_one',
+ 'description': 'path matcher one',
+ 'defaultService': 'bes-pathmatcher-one-default',
+ 'pathRules': [
+ {
+ 'service': 'my-one-bes',
+ 'paths': [
+ '/',
+ '/aboutus'
+ ]
+ }
+ ]
+ },
+ {
+ 'name': 'path_matcher_two',
+ 'description': 'path matcher two',
+ 'defaultService': 'bes-pathmatcher-two-default',
+ 'pathRules': [
+ {
+ 'service': 'my-two-bes',
+ 'paths': [
+ '/webapp',
+ '/graphs'
+ ]
+ }
+ ]
+ }
+ ]
+ }
+
+ def test__build_path_matchers(self):
+ input_list = [
+ {
+ 'defaultService': 'bes-pathmatcher-one-default',
+ 'description': 'path matcher one',
+ 'name': 'path_matcher_one',
+ 'pathRules': [
+ {
+ 'paths': [
+ '/',
+ '/aboutus'
+ ],
+ 'service': 'my-one-bes'
+ }
+ ]
+ },
+ {
+ 'defaultService': 'bes-pathmatcher-two-default',
+ 'description': 'path matcher two',
+ 'name': 'path_matcher_two',
+ 'pathRules': [
+ {
+ 'paths': [
+ '/webapp',
+ '/graphs'
+ ],
+ 'service': 'my-two-bes'
+ }
+ ]
+ }
+ ]
+ expected = [
+ {
+ 'defaultService': 'https://www.googleapis.com/compute/v1/projects/my-project/global/backendServices/bes-pathmatcher-one-default',
+ 'description': 'path matcher one',
+ 'name': 'path_matcher_one',
+ 'pathRules': [
+ {
+ 'paths': [
+ '/',
+ '/aboutus'
+ ],
+ 'service': 'https://www.googleapis.com/compute/v1/projects/my-project/global/backendServices/my-one-bes'
+ }
+ ]
+ },
+ {
+ 'defaultService': 'https://www.googleapis.com/compute/v1/projects/my-project/global/backendServices/bes-pathmatcher-two-default',
+ 'description': 'path matcher two',
+ 'name': 'path_matcher_two',
+ 'pathRules': [
+ {
+ 'paths': [
+ '/webapp',
+ '/graphs'
+ ],
+ 'service': 'https://www.googleapis.com/compute/v1/projects/my-project/global/backendServices/my-two-bes'
+ }
+ ]
+ }
+ ]
+ actual = _build_path_matchers(input_list, 'my-project')
+ self.assertEqual(expected, actual)
+
+ def test__build_url_map_dict(self):
+
+ expected = {
+ 'description': 'foo_url_map description',
+ 'hostRules': [
+ {
+ 'description': 'host rules description',
+ 'hosts': [
+ 'www.example.com',
+ 'www2.example.com'
+ ],
+ 'pathMatcher': 'host_rules_path_matcher'
+ }
+ ],
+ 'name': 'foo_url_map_name',
+ 'pathMatchers': [
+ {
+ 'defaultService': 'https://www.googleapis.com/compute/v1/projects/my-project/global/backendServices/bes-pathmatcher-one-default',
+ 'description': 'path matcher one',
+ 'name': 'path_matcher_one',
+ 'pathRules': [
+ {
+ 'paths': [
+ '/',
+ '/aboutus'
+ ],
+ 'service': 'https://www.googleapis.com/compute/v1/projects/my-project/global/backendServices/my-one-bes'
+ }
+ ]
+ },
+ {
+ 'defaultService': 'https://www.googleapis.com/compute/v1/projects/my-project/global/backendServices/bes-pathmatcher-two-default',
+ 'description': 'path matcher two',
+ 'name': 'path_matcher_two',
+ 'pathRules': [
+ {
+ 'paths': [
+ '/webapp',
+ '/graphs'
+ ],
+ 'service': 'https://www.googleapis.com/compute/v1/projects/my-project/global/backendServices/my-two-bes'
+ }
+ ]
+ }
+ ]
+ }
+ actual = _build_url_map_dict(self.params_dict, 'my-project')
+ self.assertEqual(expected, actual)