#!/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 version_added: "1.3" short_description: create, delete, or modify an Amazon rds instance description: - Creates, deletes, or modifies rds instances. When creating an instance it can be either a new instance or a read-only replica of an existing instance. This module has a dependency on python-boto >= 2.5. The 'promote' command requires boto >= 2.18.0. options: command: description: - Specifies the action to take. required: true default: null aliases: [] choices: [ 'create', 'replicate', 'delete', 'facts', 'modify' , 'promote', 'snapshot', 'restore' ] instance_name: description: - Database instance identifier. required: true default: null aliases: [] source_instance: description: - Name of the database to replicate. Used only when command=replicate. required: false default: null aliases: [] db_engine: description: - The type of database. Used only when command=create. required: false default: null aliases: [] choices: [ 'MySQL', 'oracle-se1', 'oracle-se', 'oracle-ee', 'sqlserver-ee', 'sqlserver-se', 'sqlserver-ex', 'sqlserver-web', 'postgres'] size: description: - Size in gigabytes of the initial storage for the DB instance. Used only when command=create or command=modify. required: false default: null aliases: [] instance_type: description: - The instance type of the database. Must be specified when command=create. Optional when command=replicate, command=modify or command=restore. If not specified then the replica inherits the same instance type as the source instance. required: false default: null aliases: [] choices: [ 'db.t1.micro', 'db.m1.small', 'db.m1.medium', 'db.m1.large', 'db.m1.xlarge', 'db.m2.xlarge', 'db.m2.2xlarge', 'db.m2.4xlarge', 'db.m3.medium', 'db.m3.large', 'db.m3.xlarge', 'db.m3.2xlarge', 'db.cr1.8xlarge' ] username: description: - Master database username. Used only when command=create. required: false default: null aliases: [] password: description: - Password for the master database username. Used only when command=create or command=modify. 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' ] db_name: description: - Name of a database to create within the instance. If not specified then no database is created. Used only when command=create. required: false default: null aliases: [] engine_version: description: - Version number of the database engine to use. Used only when command=create. If not specified then the current Amazon RDS default engine version is used. required: false default: null aliases: [] parameter_group: description: - Name of the DB parameter group to associate with this instance. If omitted then the RDS default DBParameterGroup will be used. Used only when command=create or command=modify. required: false default: null aliases: [] license_model: description: - The license model for this DB instance. Used only when command=create or command=restore. required: false default: null aliases: [] choices: [ 'license-included', 'bring-your-own-license', 'general-public-license' ] multi_zone: description: - Specifies if this is a Multi-availability-zone deployment. Can not be used in conjunction with zone parameter. Used only when command=create or command=modify. choices: [ "yes", "no" ] required: false default: null aliases: [] iops: description: - Specifies the number of IOPS for the instance. Used only when command=create or command=modify. Must be an integer greater than 1000. required: false default: null aliases: [] security_groups: description: - Comma separated list of one or more security groups. Used only when command=create or command=modify. required: false default: null aliases: [] vpc_security_groups: description: - Comma separated list of one or more vpc security group ids. Also requires `subnet` to be specified. Used only when command=create or command=modify. required: false default: null aliases: [] port: description: - Port number that the DB instance uses for connections. Defaults to 3306 for mysql, 1521 for Oracle, 1443 for SQL Server. Used only when command=create or command=replicate. required: false default: null aliases: [] upgrade: description: - Indicates that minor version upgrades should be applied automatically. Used only when command=create or command=replicate. required: false default: no choices: [ "yes", "no" ] aliases: [] option_group: description: - The name of the option group to use. If not specified then the default option group is used. Used only when command=create. required: false default: null aliases: [] maint_window: description: - "Maintenance window in format of ddd:hh24:mi-ddd:hh24:mi. (Example: Mon:22:00-Mon:23:15) If not specified then a random maintenance window is assigned. Used only when command=create or command=modify." required: false default: null aliases: [] backup_window: description: - Backup window in format of hh24:mi-hh24:mi. If not specified then a random backup window is assigned. Used only when command=create or command=modify. required: false default: null aliases: [] backup_retention: description: - "Number of days backups are retained. Set to 0 to disable backups. Default is 1 day. Valid range: 0-35. Used only when command=create or command=modify." required: false default: null aliases: [] zone: description: - availability zone in which to launch the instance. Used only when command=create, command=replicate or command=restore. required: false default: null aliases: ['aws_zone', 'ec2_zone'] subnet: description: - VPC subnet group. If specified then a VPC instance is created. Used only when command=create. required: false default: null aliases: [] snapshot: description: - Name of snapshot to take. When command=delete, if no snapshot name is provided then no snapshot is taken. Used only when command=delete or command=snapshot. required: false default: null aliases: [] 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' ] 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' ] wait: description: - When command=create, replicate, modify or restore then wait for the database to enter the 'available' state. When command=delete wait for the database to be terminated. required: false default: "no" choices: [ "yes", "no" ] aliases: [] wait_timeout: description: - how long before wait gives up, in seconds default: 300 aliases: [] apply_immediately: description: - Used only when command=modify. If enabled, the modifications will be applied as soon as possible rather than waiting for the next preferred maintenance window. default: no choices: [ "yes", "no" ] aliases: [] new_instance_name: description: - Name to rename an instance to. Used only when command=modify. required: false default: null aliases: [] version_added: 1.5 requirements: [ "boto" ] author: Bruce Pennypacker ''' EXAMPLES = ''' # Basic mysql provisioning example - rds: > command=create instance_name=new_database db_engine=MySQL size=10 instance_type=db.m1.small username=mysql_admin password=1nsecure # Create a read-only replica and wait for it to become available - rds: > command=replicate instance_name=new_database_replica source_instance=new_database wait=yes wait_timeout=600 # Delete an instance, but create a snapshot before doing so - rds: > command=delete instance_name=new_database snapshot=new_database_snapshot # Get facts about an instance - rds: > command=facts instance_name=new_database register: new_database_facts # Rename an instance and wait for the change to take effect - rds: > command=modify instance_name=new_database new_instance_name=renamed_database wait=yes ''' import sys import time try: import boto.rds except ImportError: print "failed=True msg='boto required for this module'" sys.exit(1) def get_current_resource(conn, resource, command): # There will be exceptions but we want the calling code to handle them if command == 'snapshot': return conn.get_all_dbsnapshots(snapshot_id=resource)[0] else: return conn.get_all_dbinstances(resource)[0] def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( command = dict(choices=['create', 'replicate', 'delete', 'facts', 'modify', 'promote', 'snapshot', 'restore'], required=True), instance_name = dict(required=True), source_instance = dict(required=False), db_engine = dict(choices=['MySQL', 'oracle-se1', 'oracle-se', 'oracle-ee', 'sqlserver-ee', 'sqlserver-se', 'sqlserver-ex', 'sqlserver-web', 'postgres'], required=False), size = dict(required=False), instance_type = dict(aliases=['type'], choices=['db.t1.micro', 'db.m1.small', 'db.m1.medium', 'db.m1.large', 'db.m1.xlarge', 'db.m2.xlarge', 'db.m2.2xlarge', 'db.m2.4xlarge', 'db.m3.medium', 'db.m3.large', 'db.m3.xlarge', 'db.m3.2xlarge', 'db.cr1.8xlarge'], required=False), username = dict(required=False), password = dict(no_log=True, required=False), db_name = dict(required=False), engine_version = dict(required=False), parameter_group = dict(required=False), license_model = dict(choices=['license-included', 'bring-your-own-license', 'general-public-license'], required=False), multi_zone = dict(type='bool', default=False), iops = dict(required=False), security_groups = dict(required=False), vpc_security_groups = dict(required=False), port = dict(required=False), upgrade = dict(type='bool', default=False), option_group = dict(required=False), maint_window = dict(required=False), backup_window = dict(required=False), backup_retention = dict(required=False), zone = dict(aliases=['aws_zone', 'ec2_zone'], required=False), subnet = dict(required=False), wait = dict(type='bool', default=False), wait_timeout = dict(default=300), snapshot = dict(required=False), apply_immediately = dict(type='bool', default=False), new_instance_name = dict(required=False), ) ) module = AnsibleModule( argument_spec=argument_spec, ) command = module.params.get('command') instance_name = module.params.get('instance_name') source_instance = module.params.get('source_instance') db_engine = module.params.get('db_engine') size = module.params.get('size') instance_type = module.params.get('instance_type') username = module.params.get('username') password = module.params.get('password') db_name = module.params.get('db_name') engine_version = module.params.get('engine_version') parameter_group = module.params.get('parameter_group') license_model = module.params.get('license_model') multi_zone = module.params.get('multi_zone') iops = module.params.get('iops') security_groups = module.params.get('security_groups') vpc_security_groups = module.params.get('vpc_security_groups') port = module.params.get('port') upgrade = module.params.get('upgrade') option_group = module.params.get('option_group') maint_window = module.params.get('maint_window') subnet = module.params.get('subnet') backup_window = module.params.get('backup_window') backup_retention = module.params.get('module_retention') region = module.params.get('region') zone = module.params.get('zone') aws_secret_key = module.params.get('aws_secret_key') aws_access_key = module.params.get('aws_access_key') wait = module.params.get('wait') wait_timeout = int(module.params.get('wait_timeout')) snapshot = module.params.get('snapshot') apply_immediately = module.params.get('apply_immediately') new_instance_name = module.params.get('new_instance_name') 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.")) # connect to the rds endpoint 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) def invalid_security_group_type(subnet): if subnet: return 'security_groups' else: return 'vpc_security_groups' # Validate parameters for each command if command == 'create': required_vars = [ 'instance_name', 'db_engine', 'size', 'instance_type', 'username', 'password' ] invalid_vars = [ 'source_instance', 'snapshot', 'apply_immediately', 'new_instance_name' ] + [invalid_security_group_type(subnet)] elif command == 'replicate': required_vars = [ 'instance_name', 'source_instance' ] invalid_vars = [ 'db_engine', 'size', 'username', 'password', 'db_name', 'engine_version', 'parameter_group', 'license_model', 'multi_zone', 'iops', 'vpc_security_groups', 'security_groups', 'option_group', 'maint_window', 'backup_window', 'backup_retention', 'subnet', 'snapshot', 'apply_immediately', 'new_instance_name' ] elif command == 'delete': required_vars = [ 'instance_name' ] invalid_vars = [ 'db_engine', 'size', 'instance_type', 'username', 'password', 'db_name', 'engine_version', 'parameter_group', 'license_model', 'multi_zone', 'iops', 'vpc_security_groups' ,'security_groups', 'option_group', 'maint_window', 'backup_window', 'backup_retention', 'port', 'upgrade', 'subnet', 'zone' , 'source_instance', 'apply_immediately', 'new_instance_name' ] elif command == 'facts': required_vars = [ 'instance_name' ] invalid_vars = [ 'db_engine', 'size', 'instance_type', 'username', 'password', 'db_name', 'engine_version', 'parameter_group', 'license_model', 'multi_zone', 'iops', 'vpc_security_groups', 'security_groups', 'option_group', 'maint_window', 'backup_window', 'backup_retention', 'port', 'upgrade', 'subnet', 'zone', 'wait', 'source_instance' 'apply_immediately', 'new_instance_name' ] elif command == 'modify': required_vars = [ 'instance_name' ] if password: params["master_password"] = password invalid_vars = [ 'db_engine', 'username', 'db_name', 'engine_version', 'license_model', 'option_group', 'port', 'upgrade', 'subnet', 'zone', 'source_instance'] elif command == 'promote': required_vars = [ 'instance_name' ] invalid_vars = [ 'db_engine', 'size', 'username', 'password', 'db_name', 'engine_version', 'parameter_group', 'license_model', 'multi_zone', 'iops', 'vpc_security_groups', 'security_groups', 'option_group', 'maint_window', 'subnet', 'source_instance', 'snapshot', 'apply_immediately', 'new_instance_name' ] elif command == 'snapshot': required_vars = [ 'instance_name', 'snapshot'] invalid_vars = [ 'db_engine', 'size', 'username', 'password', 'db_name', 'engine_version', 'parameter_group', 'license_model', 'multi_zone', 'iops', 'vpc_security_groups', 'security_groups', 'option_group', 'maint_window', 'subnet', 'source_instance', 'apply_immediately', 'new_instance_name' ] elif command == 'restore': required_vars = [ 'instance_name', 'snapshot', 'instance_type' ] invalid_vars = [ 'db_engine', 'db_name', 'username', 'password', 'engine_version', 'option_group', 'source_instance', 'apply_immediately', 'new_instance_name', 'vpc_security_groups', 'security_groups' ] for v in required_vars: if not module.params.get(v): module.fail_json(msg = str("Parameter %s required for %s command" % (v, command))) for v in invalid_vars: if module.params.get(v): module.fail_json(msg = str("Parameter %s invalid for %s command" % (v, command))) # Package up the optional parameters params = {} if db_engine: params["engine"] = db_engine if port: params["port"] = port if db_name: params["db_name"] = db_name if parameter_group: params["param_group"] = parameter_group if zone: params["availability_zone"] = zone if maint_window: params["preferred_maintenance_window"] = maint_window if backup_window: params["preferred_backup_window"] = backup_window if backup_retention: params["backup_retention_period"] = backup_retention if multi_zone: params["multi_az"] = multi_zone if engine_version: params["engine_version"] = engine_version if upgrade: params["auto_minor_version_upgrade"] = upgrade if subnet: params["db_subnet_group_name"] = subnet if license_model: params["license_model"] = license_model if option_group: params["option_group_name"] = option_group if iops: params["iops"] = iops if security_groups: params["security_groups"] = security_groups.split(',') if vpc_security_groups: params["vpc_security_groups"] = vpc_security_groups.split(',') if new_instance_name: params["new_instance_id"] = new_instance_name changed = True if command in ['create', 'restore', 'facts']: try: result = conn.get_all_dbinstances(instance_name)[0] changed = False except boto.exception.BotoServerError, e: try: if command == 'create': result = conn.create_dbinstance(instance_name, size, instance_type, username, password, **params) if command == 'restore': result = conn.restore_dbinstance_from_dbsnapshot(snapshot, instance_name, instance_type, **params) if command == 'facts': module.fail_json(msg = "DB Instance %s does not exist" % instance_name) except boto.exception.BotoServerError, e: module.fail_json(msg = e.error_message) if command == 'snapshot': try: result = conn.get_all_dbsnapshots(snapshot)[0] changed = False except boto.exception.BotoServerError, e: try: result = conn.create_dbsnapshot(snapshot, instance_name) except boto.exception.BotoServerError, e: module.fail_json(msg = e.error_message) if command == 'delete': try: result = conn.get_all_dbinstances(instance_name)[0] if result.status == 'deleting': module.exit_json(changed=False) except boto.exception.BotoServerError, e: module.exit_json(changed=False) try: if snapshot: params["skip_final_snapshot"] = False params["final_snapshot_id"] = snapshot else: params["skip_final_snapshot"] = True result = conn.delete_dbinstance(instance_name, **params) except boto.exception.BotoServerError, e: module.fail_json(msg = e.error_message) if command == 'replicate': try: if instance_type: params["instance_class"] = instance_type result = conn.create_dbinstance_read_replica(instance_name, source_instance, **params) except boto.exception.BotoServerError, e: module.fail_json(msg = e.error_message) if command == 'modify': try: params["apply_immediately"] = apply_immediately result = conn.modify_dbinstance(instance_name, **params) except boto.exception.BotoServerError, e: module.fail_json(msg = e.error_message) if apply_immediately: if new_instance_name: # Wait until the new instance name is valid found = 0 while found == 0: instances = conn.get_all_dbinstances() for i in instances: if i.id == new_instance_name: instance_name = new_instance_name found = 1 if found == 0: time.sleep(5) else: # Wait for a few seconds since it takes a while for AWS # to change the instance from 'available' to 'modifying' time.sleep(5) if command == 'promote': try: result = conn.promote_read_replica(instance_name, **params) except boto.exception.BotoServerError, e: module.fail_json(msg = e.error_message) # If we're not waiting for a delete to complete then we're all done # so just return if command == 'delete' and not wait: module.exit_json(changed=True) try: resource = get_current_resource(conn, result.id, command) except boto.exception.BotoServerError, e: module.fail_json(msg = e.error_message) # Wait for the resource to be available if requested if wait: try: wait_timeout = time.time() + wait_timeout time.sleep(5) while wait_timeout > time.time() and resource.status != 'available': time.sleep(5) if wait_timeout <= time.time(): module.fail_json(msg = "Timeout waiting for resource %s" % resource.id) resource = get_current_resource(conn, result.id, command) except boto.exception.BotoServerError, e: # If we're waiting for an instance to be deleted then # get_all_dbinstances will eventually throw a # DBInstanceNotFound error. if command == 'delete' and e.error_code == 'DBInstanceNotFound': module.exit_json(changed=True) else: module.fail_json(msg = e.error_message) # If we got here then pack up all the instance details to send # back to ansible if command == 'snapshot': d = { 'id' : resource.id, 'create_time' : resource.snapshot_create_time, 'status' : resource.status, 'availability_zone' : resource.availability_zone, 'instance_id' : resource.instance_id, 'instance_created' : resource.instance_create_time, } try: d["snapshot_type"] = resource.snapshot_type d["iops"] = resource.iops except AttributeError, e: pass # needs boto >= 2.21.0 return module.exit_json(changed=changed, snapshot=d) d = { 'id' : resource.id, 'create_time' : resource.create_time, 'status' : resource.status, 'availability_zone' : resource.availability_zone, 'backup_retention' : resource.backup_retention_period, 'backup_window' : resource.preferred_backup_window, 'maintenance_window' : resource.preferred_maintenance_window, 'multi_zone' : resource.multi_az, 'instance_type' : resource.instance_class, 'username' : resource.master_username, 'iops' : resource.iops } # Endpoint exists only if the instance is available if resource.status == 'available' and command != 'snapshot': d["endpoint"] = resource.endpoint[0] d["port"] = resource.endpoint[1] else: d["endpoint"] = None d["port"] = None # ReadReplicaSourceDBInstanceIdentifier may or may not exist try: d["replication_source"] = resource.ReadReplicaSourceDBInstanceIdentifier except Exception, e: d["replication_source"] = None module.exit_json(changed=changed, instance=d) # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * main()