[GCE] [GCP] UrlMap module (#24422)

* [GCP] UrlMap module

This module provides support for UrlMaps on Google Cloud Platform.  UrlMaps allow users to segment requests by hostname and path and direct those requests to Backend Services.

UrlMaps are a powerful and necessary part of HTTP(S) Global Load Balancing on Google Cloud Platform.

UrlMap takes advantage of the python-api so the appropriate infrastructure has been added to module_utils.

More about UrlMaps can be found at:
https://cloud.google.com/compute/docs/load-balancing/http/url-map

UrlMap API:
https://cloud.google.com/compute/docs/reference/latest/

Google Cloud Platform HTTP(S) Cross-Region Load Balancer:
https://cloud.google.com/compute/docs/load-balancing/http/

* updated documentation, remmoved parens

* fixed tabs
pull/23667/head
Tom Melendez 7 years ago committed by Ryan Brown
parent 728d3e6c84
commit 4a5cf0b5c1

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

@ -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 <http://www.gnu.org/licenses/>.
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) <tom@supertom.com>"
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()

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

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

@ -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 ''-'', *="'

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

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

@ -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)
Loading…
Cancel
Save