From c5446d95ccef9411642a269e77368e0d1fc738ca Mon Sep 17 00:00:00 2001 From: s-hertel Date: Tue, 14 Feb 2017 15:32:42 -0500 Subject: [PATCH] [cloud] New module: AWS elasticache_parameter_group for modifying Elasticache cluster settings (#21023) Added a new module for ElastiCache. Allows users to create/modify/delete/reset cache parameter groups. --- .../amazon/elasticache_parameter_group.py | 332 ++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 lib/ansible/modules/cloud/amazon/elasticache_parameter_group.py diff --git a/lib/ansible/modules/cloud/amazon/elasticache_parameter_group.py b/lib/ansible/modules/cloud/amazon/elasticache_parameter_group.py new file mode 100644 index 00000000000..8bc7e832e39 --- /dev/null +++ b/lib/ansible/modules/cloud/amazon/elasticache_parameter_group.py @@ -0,0 +1,332 @@ +#!/usr/bin/python +# 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 = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + +DOCUMENTATION = """ +--- +module: elasticache_group_parameter +short_description: Manage cache security groups in Amazon Elasticache. +description: + - Manage cache security groups in Amazon Elasticache. + - Returns information about the specified cache cluster. +version_added: "2.3" +author: "Sloane Hertel (@s-hertel)" +options: + group_family: + description: + - The name of the cache parameter group family that the cache parameter group can be used with. + choices: ['memcached1.4', 'redis2.6', 'redis2.8', 'redis3.2'] + required: yes + type: string + name: + description: + - A user-specified name for the cache parameter group. + required: yes + type: string + description: + description: + - A user-specified description for the cache parameter group. + state: + description: + - Idempotent actions that will create/modify, destroy, or reset a cache parameter group as needed. + choices: ['present', 'absent', 'reset'] + required: true + values: + description: + - A user-specified list of parameters to reset or modify for the cache parameter group. + required: no + type: dict + default: None +""" + +EXAMPLES = """ +# Note: None of these examples set aws_access_key, aws_secret_key, or region. +# It is assumed that their matching environment variables are set. +--- +- hosts: localhost + connection: local + tasks: + - name: 'Create a test parameter group' + elasticache_parameter_group: + name: 'test-param-group' + group_family: 'redis3.2' + description: 'This is a cache parameter group' + state: 'present' + - name: 'Modify a test parameter group' + elasticache_parameter_group: + name: 'test-param-group' + values: + - ['activerehashing', 'yes'] + - ['client-output-buffer-limit-normal-hard-limit', 4] + state: 'present' + - name: 'Reset all modifiable parameters for the test parameter group' + elasticache_parameter_group: + name: 'test-param-group' + state: reset + - name: 'Delete a test parameter group' + elasticache_parameter_group: + name: 'test-param-group' + state: 'absent' +""" + +RETURN = """ +elasticache: + description: cache parameter group information and response metadata + returned: always + type: dict + sample: + cache_parameter_group: + cache_parameter_group_family: redis3.2 + cache_parameter_group_name: test-please-delete + description: "initial description" + response_metadata: + http_headers: + content-length: "562" + content-type: text/xml + date: "Mon, 06 Feb 2017 22:14:08 GMT" + x-amzn-requestid: 947291f9-ecb9-11e6-85bd-3baa4eca2cc1 + http_status_code: 200 + request_id: 947291f9-ecb9-11e6-85bd-3baa4eca2cc1 + retry_attempts: 0 +changed: + description: if the cache parameter group has changed + returned: always + type: bool + sample: + changed: true +""" + +# import module snippets +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import boto3_conn, get_aws_connection_info, ec2_argument_spec, camel_dict_to_snake_dict +from ansible.module_utils.six import text_type +import traceback + +try: + import boto3 + import botocore + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + +def create(module, conn, name, group_family, description): + """ Create ElastiCache parameter group. """ + try: + response = conn.create_cache_parameter_group(CacheParameterGroupName=name, CacheParameterGroupFamily=group_family, Description=description) + changed = True + except boto.exception.BotoServerError as e: + module.fail_json(msg="Unable to create cache parameter group.", exception=traceback.format_exc(e)) + return response, changed + +def delete(module, conn, name): + """ Delete ElastiCache parameter group. """ + try: + conn.delete_cache_parameter_group(CacheParameterGroupName=name) + response = {} + changed = True + except boto.exception.BotoServerError as e: + module.fail_json(msg="Unable to delete cache parameter group.", exception=traceback.format_exc(e)) + return response, changed + +def make_current_modifiable_param_dict(module, conn, name): + """ Gets the current state of the cache parameter group and creates a dict with the format: {ParameterName: [Allowed_Values, DataType, ParameterValue]}""" + current_info = get_info(conn, name) + if current_info is False: + module.fail_json(msg="Could not connect to the cache parameter group %s." % name) + + parameters = current_info["Parameters"] + modifiable_params = {} + + for param in parameters: + if param["IsModifiable"] and ("AllowedValues" and "ParameterValue") in param: + modifiable_params[param["ParameterName"]] = [param["AllowedValues"], param["DataType"], param["ParameterValue"]] + return modifiable_params + +def check_valid_modification(module, values, modifiable_params): + """ Check if the parameters and values in values are valid. """ + changed_with_update = False + + for parameter in values: + new_value = values[parameter] + + # check valid modifiable parameters + if parameter not in modifiable_params: + module.fail_json("%s is not a modifiable parameter. Valid parameters to modify are: %s." % (parameter, modifiable_params.keys())) + + # check allowed datatype for modified parameters + str_to_type = {"integer": int, "string": text_type} + if not isinstance(new_value, str_to_type[modifiable_params[parameter][1]]): + module.fail_json(msg="%s (type %s) is not an allowed value for the parameter %s. Expected a type %s." % + (new_value, type(new_value), parameter, modifiable_params[parameter][1])) + + # check allowed values for modifiable parameters + if text_type(new_value) not in modifiable_params[parameter][0] and not isinstance(new_value, int): + module.fail_json(msg="%s is not an allowed value for the parameter %s. Valid parameters are: %s." % + (new_value, parameter, modifiable_params[parameter][0])) + + # check if a new value is different from current value + if text_type(new_value) != modifiable_params[parameter][2]: + changed_with_update = True + + return changed_with_update + +def check_changed_parameter_values(values, old_parameters, new_parameters): + """ Checking if the new values are different than the old values. """ + changed_with_update = False + + # if the user specified parameters to reset, only check those for change + if values: + for parameter in values: + if old_parameters[parameter] != new_parameters[parameter]: + changed_with_update = True + break + # otherwise check all to find a change + else: + for parameter in old_parameters: + if old_parameters[parameter] != new_parameters[parameter]: + changed_with_update = True + break + + return changed_with_update + +def modify(module, conn, name, values): + """ Modify ElastiCache parameter group to reflect the new information if it differs from the current. """ + # compares current group parameters with the parameters we've specified to to a value to see if this will change the group + format_parameters = [] + for key in values: + value = text_type(values[key]) + format_parameters.append({'ParameterName': key, 'ParameterValue': value}) + try: + response = conn.modify_cache_parameter_group(CacheParameterGroupName=name, ParameterNameValues=format_parameters) + except boto.exception.BotoServerError as e: + module.fail_json(msg="Unable to modify cache parameter group.", exception=traceback.format_exc(e)) + return response + +def reset(module, conn, name, values): + """ Reset ElastiCache parameter group if the current information is different from the new information. """ + # used to compare with the reset parameters' dict to see if there have been changes + old_parameters_dict = make_current_modifiable_param_dict(module, conn, name) + + format_parameters = [] + + # determine whether to reset all or specific parameters + if values: + all_parameters = False + format_parameters = [] + for key in values: + value = text_type(values[key]) + format_parameters.append({'ParameterName': key, 'ParameterValue': value}) + else: + all_parameters = True + + try: + response = conn.reset_cache_parameter_group(CacheParameterGroupName=name, ParameterNameValues=format_parameters, ResetAllParameters=all_parameters) + except boto.exception.BotoServerError as e: + module.fail_json(msg="Unable to reset cache parameter group.", exception=traceback.format_exc(e)) + + # determine changed + new_parameters_dict = make_current_modifiable_param_dict(module, conn, name) + changed = check_changed_parameter_values(values, old_parameters_dict, new_parameters_dict) + + return response, changed + +def get_info(conn, name): + """ Gets info about the ElastiCache parameter group. Returns false if it doesn't exist or we don't have access. """ + try: + data = conn.describe_cache_parameters(CacheParameterGroupName=name) + return data + except botocore.exceptions.ClientError as e: + return False + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + group_family=dict(type='str', choices=['memcached1.4', 'redis2.6', 'redis2.8', 'redis3.2']), + name=dict(required=True, type='str'), + description=dict(type='str'), + state=dict(required=True), + values=dict(type='dict'), + ) + ) + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO3: + module.fail_json(msg='boto required for this module') + + parameter_group_family = module.params.get('group_family') + parameter_group_name = module.params.get('name') + group_description = module.params.get('description') + state = module.params.get('state') + values = module.params.get('values') + + # Retrieve any AWS settings from the environment. + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if not region: + module.fail_json(msg="Either region or AWS_REGION or EC2_REGION environment variable or boto config aws_region or ec2_region must be set.") + + connection = boto3_conn(module, conn_type='client', + resource='elasticache', region=region, + endpoint=ec2_url, **aws_connect_kwargs) + + exists = get_info(connection, parameter_group_name) + + # check that the needed requirements are available + if state == 'present' and not exists and not (parameter_group_family or group_description): + module.fail_json(msg="Creating a group requires a family group and a description.") + elif state == 'reset' and not exists: + module.fail_json(msg="No group %s to reset. Please create the group before using the state 'reset'." % parameter_group_name) + + # Taking action + changed = False + if state == 'present': + if exists: + # confirm that the group exists without any actions + if not values: + response = exists + changed = False + # modify existing group + else: + modifiable_params = make_current_modifiable_param_dict(module, connection, parameter_group_name) + changed = check_valid_modification(module, values, modifiable_params) + response = modify(module, connection, parameter_group_name, values) + # create group + else: + response, changed = create(module, connection, parameter_group_name, parameter_group_family, group_description) + if values: + modifiable_params = make_current_modifiable_param_dict(module, connection, parameter_group_name) + changed = check_valid_modification(module, values, modifiable_params) + response = modify(module, connection, parameter_group_name, values) + elif state == 'absent': + if exists: + # delete group + response, changed = delete(module, connection, parameter_group_name) + else: + response = {} + changed = False + elif state == 'reset': + response, changed = reset(module, connection, parameter_group_name, values) + + facts_result = dict(changed=changed, elasticache=camel_dict_to_snake_dict(response)) + + module.exit_json(**facts_result) + +if __name__ == '__main__': + main()