From c99c3b2b5d396b2cc1d25ad9aa6816aa922ffbc7 Mon Sep 17 00:00:00 2001 From: Tom Melendez Date: Thu, 18 May 2017 09:49:50 -0700 Subject: [PATCH] [GCP] Healthcheck module (#24564) * [GCP] Healthcheck module * fix return YAML block * removed update_ return value; removed python26 check; typos and docs updates * doc fix * Updated int test for no-update conditions * added filter_gcp_fields test * fixed bug in update where dictionary wasn't built correctly and port was not being set. * added default values to documentation block --- .../modules/cloud/google/gcp_healthcheck.py | 469 ++++++++++++++++++ test/integration/gce.yml | 1 + .../test_gcp_healthcheck/defaults/main.yml | 7 + .../roles/test_gcp_healthcheck/tasks/main.yml | 176 +++++++ test/units/module_utils/gcp/test_utils.py | 29 ++ 5 files changed, 682 insertions(+) create mode 100644 lib/ansible/modules/cloud/google/gcp_healthcheck.py create mode 100644 test/integration/roles/test_gcp_healthcheck/defaults/main.yml create mode 100644 test/integration/roles/test_gcp_healthcheck/tasks/main.yml diff --git a/lib/ansible/modules/cloud/google/gcp_healthcheck.py b/lib/ansible/modules/cloud/google/gcp_healthcheck.py new file mode 100644 index 00000000000..0b3ae9355df --- /dev/null +++ b/lib/ansible/modules/cloud/google/gcp_healthcheck.py @@ -0,0 +1,469 @@ +#!/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_healthcheck +version_added: "2.4" +short_description: Create, Update or Destroy a Healthcheck. +description: + - Create, Update or Destroy a Healthcheck. Currently only HTTP and + HTTPS Healthchecks are supported. Healthchecks are used to monitor + individual instances, managed instance groups and/or backend + services. Healtchecks are reusable. + - Visit + U(https://cloud.google.com/compute/docs/load-balancing/health-checks) + for an overview of Healthchecks on GCP. + - See + U(https://cloud.google.com/compute/docs/reference/latest/httpHealthChecks) for + API details on HTTP Healthchecks. + - See + U(https://cloud.google.com/compute/docs/reference/latest/httpsHealthChecks) + for more details on the HTTPS Healtcheck API. +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 HTTP and HTTPS Healthchecks currently. +author: + - "Tom Melendez (@supertom) " +options: + check_interval: + description: + - How often (in seconds) to send a health check. + required: false + default: 5 + healthcheck_name: + description: + - Name of the Healthcheck. + required: true + healthcheck_type: + description: + - Type of Healthcheck. + required: true + choices: ["HTTP", "HTTPS"] + host_header: + description: + - The value of the host header in the health check request. If left + empty, the public IP on behalf of which this health + check is performed will be used. + required: true + default: "" + port: + description: + - The TCP port number for the health check request. The default value is + 443 for HTTPS and 80 for HTTP. + required: false + request_path: + description: + - The request path of the HTTPS health check request. + required: false + default: "/" + state: + description: State of the Healthcheck. + required: true + choices: ["present", "absent"] + timeout: + description: + - How long (in seconds) to wait for a response before claiming + failure. It is invalid for timeout + to have a greater value than check_interval. + required: false + default: 5 + unhealthy_threshold: + description: + - A so-far healthy instance will be marked unhealthy after this + many consecutive failures. + required: false + default: 2 + healthy_threshold: + description: + - A so-far unhealthy instance will be marked healthy after this + many consecutive successes. + required: false + default: 2 + service_account_email: + description: + - service account email + required: false + default: null + service_account_permissions: + version_added: "2.0" + description: + - service account permissions (see + U(https://cloud.google.com/sdk/gcloud/reference/compute/instances/create), + --scopes section for detailed information) + required: false + default: null + choices: [ + "bigquery", "cloud-platform", "compute-ro", "compute-rw", + "useraccounts-ro", "useraccounts-rw", "datastore", "logging-write", + "monitoring", "sql-admin", "storage-full", "storage-ro", + "storage-rw", "taskqueue", "userinfo-email" + ] + credentials_file: + description: + - Path to the JSON file associated with the service account email + default: null + required: false + project_id: + description: + - Your GCP project ID + required: false + default: null +''' + +EXAMPLES = ''' +- name: Create Minimum HealthCheck + gcp_healthcheck: + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ project_id }}" + healthcheck_name: my-healthcheck + healthcheck_type: HTTP + state: present +- name: Create HTTP HealthCheck + gcp_healthcheck: + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ project_id }}" + healthcheck_name: my-healthcheck + healthcheck_type: HTTP + host: my-host + request_path: /hc + check_interval: 10 + timeout: 30 + unhealthy_threshhold: 2 + healthy_threshhold: 1 + state: present +- name: Create HTTPS HealthCheck + gcp_healthcheck: + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ project_id }}" + healthcheck_name: "{{ https_healthcheck }}" + healthcheck_type: HTTPS + host_header: my-host + request_path: /hc + check_interval: 5 + timeout: 5 + unhealthy_threshold: 2 + healthy_threshold: 1 + state: present +''' + +RETURN = ''' +state: + description: state of the Healthcheck + returned: Always. + type: str + sample: present +healthcheck_name: + description: Name of the Healthcheck + returned: Always + type: str + sample: my-url-map +healthcheck_type: + description: Type of the Healthcheck + returned: Always + type: str + sample: HTTP +healthcheck: + description: GCP Healthcheck dictionary + returned: Always. Refer to GCP documentation for detailed field descriptions. + type: dict + sample: { "name": "my-hc", "port": 443, "requestPath": "/foo" } +''' + + +# 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-healthcheck' +USER_AGENT_VERSION = '0.0.1' + + +def _validate_healthcheck_params(params): + """ + Validate healthcheck params. + + Simple validation has already assumed by AnsibleModule. + + :param params: Ansible dictionary containing configuration. + :type params: ``dict`` + + :return: True or raises ValueError + :rtype: ``bool`` or `class:ValueError` + """ + if params['timeout'] > params['check_interval']: + raise ValueError("timeout (%s) is greater than check_interval (%s)" % ( + params['timeout'], params['check_interval'])) + + return (True, '') + + +def _build_healthcheck_dict(params): + """ + Reformat services in Ansible Params for GCP. + + :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 + HealthCheck (HTTP/HTTPS) API. + :rtype ``dict`` + """ + gcp_dict = GCPUtils.params_to_gcp_dict(params, 'healthcheck_name') + if 'timeout' in gcp_dict: + gcp_dict['timeoutSec'] = gcp_dict['timeout'] + del gcp_dict['timeout'] + + if 'checkInterval' in gcp_dict: + gcp_dict['checkIntervalSec'] = gcp_dict['checkInterval'] + del gcp_dict['checkInterval'] + + if 'hostHeader' in gcp_dict: + gcp_dict['host'] = gcp_dict['hostHeader'] + del gcp_dict['hostHeader'] + + if 'healthcheckType' in gcp_dict: + del gcp_dict['healthcheckType'] + return gcp_dict + + +def _get_req_resource(client, resource_type): + if resource_type == 'HTTPS': + return (client.httpsHealthChecks(), 'httpsHealthCheck') + else: + return (client.httpHealthChecks(), 'httpHealthCheck') + + +def get_healthcheck(client, name, project_id=None, resource_type='HTTP'): + """ + Get a Healthcheck 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: + resource, entity_name = _get_req_resource(client, resource_type) + args = {'project': project_id, entity_name: name} + req = resource.get(**args) + return GCPUtils.execute_api_client_req(req, raise_404=False) + except: + raise + + +def create_healthcheck(client, params, project_id, resource_type='HTTP'): + """ + Create a new Healthcheck. + + :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_healthcheck_dict(params) + try: + resource, _ = _get_req_resource(client, resource_type) + args = {'project': project_id, 'body': gcp_dict} + req = resource.insert(**args) + return_data = GCPUtils.execute_api_client_req(req, client, raw=False) + if not return_data: + return_data = get_healthcheck(client, + name=params['healthcheck_name'], + project_id=project_id) + return (True, return_data) + except: + raise + + +def delete_healthcheck(client, name, project_id, resource_type='HTTP'): + """ + Delete a Healthcheck. + + :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: + resource, entity_name = _get_req_resource(client, resource_type) + args = {'project': project_id, entity_name: name} + req = resource.delete(**args) + return_data = GCPUtils.execute_api_client_req(req, client) + return (True, return_data) + except: + raise + + +def update_healthcheck(client, healthcheck, params, name, project_id, + resource_type='HTTP'): + """ + Update a Healthcheck. + + If the healthcheck has not changed, the update will not occur. + + :param client: An initialized GCE Compute Disovery resource. + :type client: :class: `googleapiclient.discovery.Resource` + + :param healthcheck: Name of the Url Map. + :type healthcheck: ``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_healthcheck_dict(params) + ans = GCPUtils.are_params_equal(healthcheck, gcp_dict) + if ans: + return (False, 'no update necessary') + + try: + resource, entity_name = _get_req_resource(client, resource_type) + args = {'project': project_id, entity_name: name, 'body': gcp_dict} + req = resource.update(**args) + 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( + healthcheck_name=dict(required=True), + healthcheck_type=dict(required=True, + choices=['HTTP', 'HTTPS']), + request_path=dict(required=False, default='/'), + check_interval=dict(required=False, type='int', default=5), + healthy_threshold=dict(required=False, type='int', default=2), + unhealthy_threshold=dict(required=False, type='int', default=2), + host_header=dict(required=False, type='str', default=''), + timeout=dict(required=False, type='int', default=5), + port=dict(required=False, type='int'), + state=dict(choices=['absent', 'present'], default='present'), + service_account_email=dict(), + service_account_permissions=dict(type='list'), + credentials_file=dict(), + project_id=dict(), ), ) + + client, conn_params = get_google_api_client(module, 'compute', user_agent_product=USER_AGENT_PRODUCT, + user_agent_version=USER_AGENT_VERSION) + + params = {} + + params['healthcheck_name'] = module.params.get('healthcheck_name') + params['healthcheck_type'] = module.params.get('healthcheck_type') + params['request_path'] = module.params.get('request_path') + params['check_interval'] = module.params.get('check_interval') + params['healthy_threshold'] = module.params.get('healthy_threshold') + params['unhealthy_threshold'] = module.params.get('unhealthy_threshold') + params['host_header'] = module.params.get('host_header') + params['timeout'] = module.params.get('timeout') + params['port'] = module.params.get('port', None) + params['state'] = module.params.get('state') + + if not params['port']: + params['port'] = 80 + if params['healthcheck_type'] == 'HTTPS': + params['port'] = 443 + try: + _validate_healthcheck_params(params) + except Exception as e: + module.fail_json(msg=e.message, changed=False) + + changed = False + json_output = {'state': params['state']} + healthcheck = get_healthcheck(client, + name=params['healthcheck_name'], + project_id=conn_params['project_id'], + resource_type=params['healthcheck_type']) + + if not healthcheck: + if params['state'] == 'absent': + # Doesn't exist in GCE, and state==absent. + changed = False + module.fail_json( + msg="Cannot delete unknown healthcheck: %s" % + (params['healthcheck_name'])) + else: + # Create + changed, json_output['healthcheck'] = create_healthcheck(client, + params=params, + project_id=conn_params['project_id'], + resource_type=params['healthcheck_type']) + elif params['state'] == 'absent': + # Delete + changed, json_output['healthcheck'] = delete_healthcheck(client, + name=params['healthcheck_name'], + project_id=conn_params['project_id'], + resource_type=params['healthcheck_type']) + else: + changed, json_output['healthcheck'] = update_healthcheck(client, + healthcheck=healthcheck, + params=params, + name=params['healthcheck_name'], + project_id=conn_params['project_id'], + resource_type=params['healthcheck_type']) + 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 268b0257705..f19201ec138 100644 --- a/test/integration/gce.yml +++ b/test/integration/gce.yml @@ -8,4 +8,5 @@ - { 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 } + - { role: test_gcp_healthcheck, tags: test_gcp_healthcheck } # TODO: tests for gce_lb, gc_storage diff --git a/test/integration/roles/test_gcp_healthcheck/defaults/main.yml b/test/integration/roles/test_gcp_healthcheck/defaults/main.yml new file mode 100644 index 00000000000..a993cf43ea3 --- /dev/null +++ b/test/integration/roles/test_gcp_healthcheck/defaults/main.yml @@ -0,0 +1,7 @@ +--- +# defaults file for test_gcp_healthcheck +service_account_email: "{{ gce_service_account_email }}" +credentials_file: "{{ gce_pem_file }}" +project_id: "{{ gce_project_id }}" +http_healthcheck: "ans-int-healthcheck-http-{{ resource_prefix|lower }}" +https_healthcheck: "ans-int-healthcheck-https-{{ resource_prefix|lower }}" \ No newline at end of file diff --git a/test/integration/roles/test_gcp_healthcheck/tasks/main.yml b/test/integration/roles/test_gcp_healthcheck/tasks/main.yml new file mode 100644 index 00000000000..cb3d4416d08 --- /dev/null +++ b/test/integration/roles/test_gcp_healthcheck/tasks/main.yml @@ -0,0 +1,176 @@ +# GCP Healthcheck Integration Tests. +###### +# ============================================================ +- name: param check +# ============================================================ + gcp_healthcheck: + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ project_id }}" + healthcheck_name: "{{ http_healthcheck }}" + healthcheck_type: HTTP + host_header: my-host + request_path: /hc + check_interval: 10 + timeout: 30 + unhealthy_threshold: 2 + healthy_threshold: 1 + state: present + ignore_errors: True + register: result +- name: check interval + assert: + that: + 'result.msg == "timeout (30) is greater than check_interval (10)"' +# ============================================================ +- name: create "{{ http_healthcheck }}" +# ============================================================ + gcp_healthcheck: + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ project_id }}" + healthcheck_name: "{{ http_healthcheck }}" + healthcheck_type: HTTP + host_header: my-host + request_path: /hc + check_interval: 5 + timeout: 5 + unhealthy_threshold: 2 + healthy_threshold: 1 + state: present + register: result +- name: assert create + assert: + that: + - 'result.state == "present"' + - 'result.changed' +# ============================================================ +- name: "update {{ http_healthcheck }}, no change" +# ============================================================ + gcp_healthcheck: + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ project_id }}" + healthcheck_name: "{{ http_healthcheck }}" + healthcheck_type: HTTP + host_header: my-host + request_path: /hc + check_interval: 5 + timeout: 5 + unhealthy_threshold: 2 + healthy_threshold: 1 + state: present + register: result +- name: assert update no change + assert: + that: + - 'result.state == "present"' + - 'not result.changed' + - 'result.port == 80' +# ============================================================ +- name: create minimum "{{ https_healthcheck }}" +# ============================================================ + gcp_healthcheck: + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ project_id }}" + healthcheck_name: "{{ https_healthcheck }}" + healthcheck_type: HTTPS + state: present + register: result +- name: assert create + assert: + that: + - 'result.state == "present"' + - 'result.changed' +# ============================================================ +- name: "update {{ https_healthcheck }}, no change" +# ============================================================ + gcp_healthcheck: + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ project_id }}" + healthcheck_name: "{{ https_healthcheck }}" + healthcheck_type: HTTPS + state: present + register: result +- name: assert not updated + assert: + that: + - 'result.state == "present"' + - 'result.port == 443' + - 'not result.changed' +# ============================================================ +- name: update "{{ https_healthcheck }}" +# ============================================================ + gcp_healthcheck: + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ project_id }}" + healthcheck_name: "{{ https_healthcheck }}" + healthcheck_type: HTTPS + host_header: my-host + request_path: /hc + check_interval: 5 + timeout: 5 + unhealthy_threshold: 2 + healthy_threshold: 1 + port: 444 + state: present + register: result +- name: assert update "{{ https_healthcheck }}" + assert: + that: + - 'result.state == "present"' + - 'result.changed' + - 'result.port == 444' +# ============================================================ +- pause: seconds=5 +# ============================================================ +- name: delete "{{ http_healthcheck }}" +# ============================================================ + gcp_healthcheck: + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ project_id }}" + healthcheck_name: "{{ http_healthcheck }}" + healthcheck_type: HTTP + host_header: my-host + request_path: /hc + check_interval: 5 + timeout: 5 + unhealthy_threshold: 2 + healthy_threshold: 1 + state: absent + register: result + tags: + - delete +- name: assert absent + assert: + that: + - 'result.state == "absent"' + - 'result.changed' +# ============================================================ +- name: delete "{{ https_healthcheck }}" +# ============================================================ + gcp_healthcheck: + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ project_id }}" + healthcheck_name: "{{ https_healthcheck }}" + healthcheck_type: HTTPS + host_header: my-host + request_path: /hc + check_interval: 5 + timeout: 5 + unhealthy_threshold: 2 + healthy_threshold: 1 + state: absent + register: result + tags: + - delete +- name: assert absent + assert: + that: + - 'result.state == "absent"' + - 'result.changed' diff --git a/test/units/module_utils/gcp/test_utils.py b/test/units/module_utils/gcp/test_utils.py index 98425219492..e2b0feb612e 100644 --- a/test/units/module_utils/gcp/test_utils.py +++ b/test/units/module_utils/gcp/test_utils.py @@ -342,3 +342,32 @@ class GCPUtilsTestCase(unittest.TestCase): # params1 has exclude fields, params2 doesn't. Should be equal actual = GCPUtils.are_params_equal(params1, params2) self.assertTrue(actual) + + def test_filter_gcp_fields(self): + input_data = { + u'kind': u'compute#httpsHealthCheck', + u'description': u'', + u'timeoutSec': 5, + u'checkIntervalSec': 5, + u'port': 443, + u'healthyThreshold': 2, + u'host': u'', + u'requestPath': u'/', + u'unhealthyThreshold': 2, + u'creationTimestamp': u'2017-05-16T15:09:36.546-07:00', + u'id': u'8727093129334146639', + u'selfLink': u'https://www.googleapis.com/compute/v1/projects/myproject/global/httpsHealthChecks/myhealthcheck', + u'name': u'myhealthcheck'} + + expected = { + 'name': 'myhealthcheck', + 'checkIntervalSec': 5, + 'port': 443, + 'unhealthyThreshold': 2, + 'healthyThreshold': 2, + 'host': '', + 'timeoutSec': 5, + 'requestPath': '/'} + + actual = GCPUtils.filter_gcp_fields(input_data) + self.assertEquals(expected, actual)