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)
This commit is contained in:
Will Thames 2013-12-24 17:45:10 +10:00
parent 643690ffec
commit b65486734a

View file

@ -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 *