From 5c9739ae9ccd838daed8c3cb9b9333833f412f93 Mon Sep 17 00:00:00 2001 From: Scott Anderson Date: Sun, 2 Mar 2014 19:36:07 -0500 Subject: [PATCH] Adds modules for managing Amazon RDS parameter and subnet groups. --- library/cloud/rds_param_group | 299 +++++++++++++++++++++++++++++++++ library/cloud/rds_subnet_group | 166 ++++++++++++++++++ 2 files changed, 465 insertions(+) create mode 100644 library/cloud/rds_param_group create mode 100644 library/cloud/rds_subnet_group diff --git a/library/cloud/rds_param_group b/library/cloud/rds_param_group new file mode 100644 index 0000000000..83f11e4c61 --- /dev/null +++ b/library/cloud/rds_param_group @@ -0,0 +1,299 @@ +#!/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 . + +DOCUMENTATION = ''' +--- +module: rds_param_group +version_added: "1.5" +short_description: manage RDS parameter groups +description: + - Creates, modifies, and deletes RDS parameter groups. This module has a dependency on python-boto >= 2.5. +options: + state: + description: + - Specifies whether the group should be present or absent. + required: true + default: present + aliases: [] + choices: [ 'present' , 'absent' ] + name: + description: + - Database parameter group identifier. + required: true + default: null + aliases: [] + description: + description: + - Database parameter group description. Only set when a new group is added. + required: false + default: null + aliases: [] + engine: + description: + - The type of database for this group. Required for state=present. + required: false + default: null + aliases: [] + choices: [ 'mysql5.1', 'mysql5.5', 'mysql5.6', 'oracle-ee-11.2', 'oracle-se-11.2', 'oracle-se1-11.2', 'postgres9.3', 'sqlserver-ee-10.5', 'sqlserver-ee-11.0', 'sqlserver-ex-10.5', 'sqlserver-ex-11.0', 'sqlserver-se-10.5', 'sqlserver-se-11.0', 'sqlserver-web-10.5', 'sqlserver-web-11.0'] + immediate: + description: + - Whether to apply the changes immediately, or after the next reboot of any associated instances. + required: false + default: null + aliases: [] + params: + description: + - Map of parameter names and values. Numeric values may be represented as K for kilo (1024), M for mega (1024^2), G for giga (1024^3), or T for tera (1024^4), and these values will be expanded into the appropriate number before being set in the parameter group. + required: false + default: null + aliases: [] + choices: [ 'mysql5.1', 'mysql5.5', 'mysql5.6', 'oracle-ee-11.2', 'oracle-se-11.2', 'oracle-se1-11.2', 'postgres9.3', 'sqlserver-ee-10.5', 'sqlserver-ee-11.0', 'sqlserver-ex-10.5', 'sqlserver-ex-11.0', 'sqlserver-se-10.5', 'sqlserver-se-11.0', 'sqlserver-web-10.5', 'sqlserver-web-11.0'] + region: + description: + - The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used. + required: true + default: null + aliases: [ 'aws_region', 'ec2_region' ] + aws_access_key: + description: + - AWS access key. If not set then the value of the AWS_ACCESS_KEY environment variable is used. + required: false + default: null + aliases: [ 'ec2_access_key', 'access_key' ] + aws_secret_key: + description: + - AWS secret key. If not set then the value of the AWS_SECRET_KEY environment variable is used. + required: false + default: null + aliases: [ 'ec2_secret_key', 'secret_key' ] +requirements: [ "boto" ] +author: Scott Anderson +''' + +EXAMPLES = ''' +# Add or change a parameter group, in this case setting auto_increment_increment to 42 * 1024 +- rds_param_group: > + state=present + name=norwegian_blue + description=My Fancy Ex Parrot Group + engine=mysql5.6 + params='{"auto_increment_increment": "42K"}' + +# Remove a parameter group +- rds_param_group: > + state=absent + name=norwegian_blue +''' + +import sys +import time + +VALID_ENGINES = [ + 'mysql5.1', + 'mysql5.5', + 'mysql5.6', + 'oracle-ee-11.2', + 'oracle-se-11.2', + 'oracle-se1-11.2', + 'postgres9.3', + 'sqlserver-ee-10.5', + 'sqlserver-ee-11.0', + 'sqlserver-ex-10.5', + 'sqlserver-ex-11.0', + 'sqlserver-se-10.5', + 'sqlserver-se-11.0', + 'sqlserver-web-10.5', + 'sqlserver-web-11.0', +] + +try: + import boto.rds + from boto.exception import BotoServerError +except ImportError: + print "failed=True msg='boto required for this module'" + sys.exit(1) + +# returns a tuple: (whether or not a parameter was changed, the remaining parameters that weren't found in this parameter group) + +class NotModifiableError(StandardError): + def __init__(self, error_message, *args): + super(NotModifiableError, self).__init__(error_message, *args) + self.error_message = error_message + + def __repr__(self): + return 'NotModifiableError: %s' % self.error_message + + def __str__(self): + return 'NotModifiableError: %s' % self.error_message + +INT_MODIFIERS = { + 'K': 1024, + 'M': pow(1024, 2), + 'G': pow(1024, 3), + 'T': pow(1024, 4), +} + +TRUE_VALUES = ('on', 'true', 'yes', '1',) + +def set_parameter(param, value, immediate): + """ + Allows setting parameters with 10M = 10* 1024 * 1024 and so on. + """ + converted_value = value + + if param.type == 'string': + converted_value = str(value) + + elif param.type == 'integer': + if isinstance(value, basestring): + for modifier in INT_MODIFIERS.keys(): + if value.endswith(modifier): + converted_value = int(value[:-1]) * INT_MODIFIERS[modifier] + converted_value = int(converted_value) + elif type(value) == bool: + converted_value = 1 if value else 0 + else: + converted_value = int(value) + + elif param.type == 'boolean': + if isinstance(value, basestring): + converted_value = value in TRUE_VALUES + else: + converted_value = bool(value) + + param.value = converted_value + param.apply(immediate) + +def modify_group(group, params, immediate=False): + """ Set all of the params in a group to the provided new params. Raises NotModifiableError if any of the + params to be changed are read only. + """ + changed = {} + + new_params = dict(params) + + for key in new_params.keys(): + if group.has_key(key): + param = group[key] + new_value = new_params[key] + + if param.value != new_value: + if not param.is_modifiable: + raise NotModifiableError('Parameter %s is not modifiable.' % key) + + changed[key] = {'old': param.value, 'new': new_value} + + set_parameter(param, new_value, immediate) + + del new_params[key] + + return changed, new_params + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + state = dict(required=True, choices=['present', 'absent']), + name = dict(required=True), + engine = dict(required=False, choices=VALID_ENGINES), + description = dict(required=False), + params = dict(required=False, aliases=['parameters'], type='dict'), + immediate = dict(required=False, type='bool'), + ) + ) + module = AnsibleModule(argument_spec=argument_spec) + + state = module.params.get('state') + group_name = module.params.get('name').lower() + group_engine = module.params.get('engine') + group_description = module.params.get('description') + group_params = module.params.get('params') or {} + immediate = module.params.get('immediate') or False + + if state == 'present': + for required in ['name', 'description', 'engine', 'params']: + if not module.params.get(required): + module.fail_json(msg = str("Parameter %s required for state='present'" % required)) + else: + for not_allowed in ['description', 'engine', 'params']: + if module.params.get(not_allowed): + module.fail_json(msg = str("Parameter %s not allowed for state='absent'" % not_allowed)) + + # Retrieve any AWS settings from the environment. + ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module) + + if not region: + module.fail_json(msg = str("region not specified and unable to determine region from EC2_REGION.")) + + try: + conn = boto.rds.connect_to_region(region, aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key) + except boto.exception.BotoServerError, e: + module.fail_json(msg = e.error_message) + + group_was_added = False + + try: + changed = False + + try: + all_groups = conn.get_all_dbparameter_groups(group_name, max_records=100) + exists = len(all_groups) > 0 + except BotoServerError, e: + if e.error_code != 'DBParameterGroupNotFound': + module.fail_json(msg = e.error_message) + exists = False + + if state == 'absent': + if exists: + conn.delete_parameter_group(group_name) + changed = True + else: + changed = {} + if not exists: + new_group = conn.create_parameter_group(group_name, engine=group_engine, description=group_description) + group_was_added = True + + # If a "Marker" is present, this group has more attributes remaining to check. Get the next batch, but only + # if there are parameters left to set. + marker = None + while len(group_params): + next_group = conn.get_all_dbparameters(group_name, marker=marker) + + changed_params, group_params = modify_group(next_group, group_params, immediate) + changed.update(changed_params) + + if hasattr(next_group, 'Marker'): + marker = next_group.Marker + else: + break + + + except BotoServerError, e: + module.fail_json(msg = e.error_message) + + except NotModifiableError, e: + msg = e.error_message + if group_was_added: + msg = '%s The group "%s" was added first.' % (msg, group_name) + module.fail_json(msg=msg) + + module.exit_json(changed=changed) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +main() diff --git a/library/cloud/rds_subnet_group b/library/cloud/rds_subnet_group new file mode 100644 index 0000000000..1688856719 --- /dev/null +++ b/library/cloud/rds_subnet_group @@ -0,0 +1,166 @@ +#!/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 . + +DOCUMENTATION = ''' +--- +module: rds_subnet_group +version_added: "1.5" +short_description: manage RDS database subnet groups +description: + - Creates, modifies, and deletes RDS database subnet groups. This module has a dependency on python-boto >= 2.5. +options: + state: + description: + - Specifies whether the subnet should be present or absent. + required: true + default: present + aliases: [] + choices: [ 'present' , 'absent' ] + name: + description: + - Database subnet group identifier. + required: true + default: null + aliases: [] + description: + description: + - Database subnet group description. Only set when a new group is added. + required: false + default: null + aliases: [] + subnets: + description: + - List of subnet IDs that make up the database subnet group. + required: false + default: null + aliases: [] + region: + description: + - The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used. + required: true + default: null + aliases: [ 'aws_region', 'ec2_region' ] + aws_access_key: + description: + - AWS access key. If not set then the value of the AWS_ACCESS_KEY environment variable is used. + required: false + default: null + aliases: [ 'ec2_access_key', 'access_key' ] + aws_secret_key: + description: + - AWS secret key. If not set then the value of the AWS_SECRET_KEY environment variable is used. + required: false + default: null + aliases: [ 'ec2_secret_key', 'secret_key' ] +requirements: [ "boto" ] +author: Scott Anderson +''' + +EXAMPLES = ''' +# Add or change a subnet group +- local_action: + module: rds_subnet_group + state: present + name: norwegian-blue + description: My Fancy Ex Parrot Subnet Group + subnets: + - subnet-aaaaaaaa + - subnet-bbbbbbbb + +# Remove a parameter group +- rds_param_group: > + state=absent + name=norwegian-blue +''' + +import sys +import time + +try: + import boto.rds + from boto.exception import BotoServerError +except ImportError: + print "failed=True msg='boto required for this module'" + sys.exit(1) + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + state = dict(required=True, choices=['present', 'absent']), + name = dict(required=True), + description = dict(required=False), + subnets = dict(required=False, type='list'), + ) + ) + module = AnsibleModule(argument_spec=argument_spec) + + state = module.params.get('state') + group_name = module.params.get('name').lower() + group_description = module.params.get('description') + group_subnets = module.params.get('subnets') or {} + + if state == 'present': + for required in ['name', 'description', 'subnets']: + if not module.params.get(required): + module.fail_json(msg = str("Parameter %s required for state='present'" % required)) + else: + for not_allowed in ['description', 'subnets']: + if module.params.get(not_allowed): + module.fail_json(msg = str("Parameter %s not allowed for state='absent'" % not_allowed)) + + # Retrieve any AWS settings from the environment. + ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module) + + if not region: + module.fail_json(msg = str("region not specified and unable to determine region from EC2_REGION.")) + + try: + conn = boto.rds.connect_to_region(region, aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key) + except boto.exception.BotoServerError, e: + module.fail_json(msg = e.error_message) + + try: + changed = False + exists = False + + try: + matching_groups = conn.get_all_db_subnet_groups(group_name, max_records=100) + exists = len(matching_groups) > 0 + except BotoServerError, e: + if e.error_code != 'DBSubnetGroupNotFoundFault': + module.fail_json(msg = e.error_message) + + if state == 'absent': + if exists: + conn.delete_db_subnet_group(group_name) + changed = True + else: + if not exists: + new_group = conn.create_db_subnet_group(group_name, desc=group_description, subnet_ids=group_subnets) + + else: + changed_group = conn.modify_db_subnet_group(group_name, description=group_description, subnet_ids=group_subnets) + + except BotoServerError, e: + module.fail_json(msg = e.error_message) + + module.exit_json(changed=changed) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +main()