From 5d41934873945b660156ea5912d8b97feee47eab Mon Sep 17 00:00:00 2001 From: Will Thames Date: Tue, 24 Dec 2013 17:45:10 +1000 Subject: [PATCH] rds module: add snapshot capabilities Add the ability to create snapshots and restore from them Make instance creation, deletion, restore, and snapshotting idempotent (really helps testing a playbook if you can run it multiple times) --- cloud/rds | 201 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 134 insertions(+), 67 deletions(-) diff --git a/cloud/rds b/cloud/rds index c7e2844113d..dad58bc1486 100644 --- a/cloud/rds +++ b/cloud/rds @@ -28,7 +28,7 @@ options: required: true default: null aliases: [] - choices: [ 'create', 'replicate', 'delete', 'facts', 'modify' , 'promote' ] + choices: [ 'create', 'replicate', 'delete', 'facts', 'modify' , 'promote', 'snapshot', 'restore' ] instance_name: description: - Database instance identifier. @@ -56,7 +56,7 @@ options: aliases: [] instance_type: description: - - The instance type of the database. Must be specified when command=create. Optional when command=replicate or command=modify. If not specified then the replica inherits the same instance type as the source instance. + - 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: [] @@ -99,7 +99,7 @@ options: aliases: [] license_model: description: - - The license model for this DB instance. Used only when command=create. + - The license model for this DB instance. Used only when command=create or command=restore. required: false default: null aliases: [] @@ -162,7 +162,7 @@ options: aliases: [] zone: description: - - availability zone in which to launch the instance. Used only when command=create or command=replicate. + - 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'] @@ -174,7 +174,7 @@ options: aliases: [] snapshot: description: - - Name of final snapshot to take when deleting an instance. If no snapshot name is provided then no snapshot is taken. Used only when command=delete. + - 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: [] @@ -192,7 +192,7 @@ options: aliases: [ 'ec2_access_key', 'access_key' ] wait: description: - - When command=create, replicate, or modify then wait for the database to enter the 'available' state. When command=delete wait for the database to be terminated. + - 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" ] @@ -277,10 +277,18 @@ 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(): module = AnsibleModule( argument_spec = dict( - command = dict(choices=['create', 'replicate', 'delete', 'facts', 'modify', 'promote'], required=True), + 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), @@ -400,6 +408,14 @@ def main(): 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', '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', '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', 'usernmae', 'password', 'engine_version', 'option_group', 'source_instance', 'apply_immediately', 'new_instance_name' ] for v in required_vars: if not module.params.get(v): @@ -466,74 +482,108 @@ def main(): if new_instance_name: params["new_instance_id"] = new_instance_name - try: - if command == 'create': - db = conn.create_dbinstance(instance_name, size, instance_type, username, password, **params) - elif command == 'replicate': - if instance_type: - params["instance_class"] = instance_type - db = conn.create_dbinstance_read_replica(instance_name, source_instance, **params) - elif command == 'delete': + 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) - db = conn.delete_dbinstance(instance_name, **params) - elif command == 'modify': + 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 - db = conn.modify_dbinstance(instance_name, **params) - 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) + 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) - elif command == 'promote': - db = conn.promote_read_replica(instance_name, **params) - - # Don't do anything for the 'facts' command since we'll just drop down - # to get_all_dbinstances below to collect the facts - - except boto.exception.BotoServerError, e: - module.fail_json(msg = e.error_message) + 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: - instances = conn.get_all_dbinstances(instance_name) - my_inst = instances[0] + try: + resource = get_current_resource(conn, result.id, command) except boto.exception.BotoServerError, e: module.fail_json(msg = e.error_message) - - # Wait for the instance to be available if requested + # 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 my_inst.status != 'available': + while wait_timeout > time.time() and resource.status != 'available': time.sleep(5) if wait_timeout <= time.time(): - module.fail_json(msg = "Timeout waiting for database instance %s" % instance_name) - instances = conn.get_all_dbinstances(instance_name) - my_inst = instances[0] + 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 @@ -545,35 +595,52 @@ def main(): # 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' : my_inst.id, - 'create_time' : my_inst.create_time, - 'status' : my_inst.status, - 'availability_zone' : my_inst.availability_zone, - 'backup_retention' : my_inst.backup_retention_period, - 'backup_window' : my_inst.preferred_backup_window, - 'maintenance_window' : my_inst.preferred_maintenance_window, - 'multi_zone' : my_inst.multi_az, - 'instance_type' : my_inst.instance_class, - 'username' : my_inst.master_username, - 'iops' : my_inst.iops + '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 my_inst.status == 'available': - d["endpoint"] = my_inst.endpoint[0] - d["port"] = my_inst.endpoint[1] + 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"] = my_inst.ReadReplicaSourceDBInstanceIdentifier + d["replication_source"] = resource.ReadReplicaSourceDBInstanceIdentifier except Exception, e: d["replication_source"] = None - module.exit_json(changed=True, instance=d) + module.exit_json(changed=changed, instance=d) # import module snippets from ansible.module_utils.basic import *