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 4c168abccc
commit 5d41934873

201
cloud/rds
View file

@ -28,7 +28,7 @@ options:
required: true required: true
default: null default: null
aliases: [] aliases: []
choices: [ 'create', 'replicate', 'delete', 'facts', 'modify' , 'promote' ] choices: [ 'create', 'replicate', 'delete', 'facts', 'modify' , 'promote', 'snapshot', 'restore' ]
instance_name: instance_name:
description: description:
- Database instance identifier. - Database instance identifier.
@ -56,7 +56,7 @@ options:
aliases: [] aliases: []
instance_type: instance_type:
description: 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 required: false
default: null default: null
aliases: [] aliases: []
@ -99,7 +99,7 @@ options:
aliases: [] aliases: []
license_model: license_model:
description: 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 required: false
default: null default: null
aliases: [] aliases: []
@ -162,7 +162,7 @@ options:
aliases: [] aliases: []
zone: zone:
description: 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 required: false
default: null default: null
aliases: ['aws_zone', 'ec2_zone'] aliases: ['aws_zone', 'ec2_zone']
@ -174,7 +174,7 @@ options:
aliases: [] aliases: []
snapshot: snapshot:
description: 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 required: false
default: null default: null
aliases: [] aliases: []
@ -192,7 +192,7 @@ options:
aliases: [ 'ec2_access_key', 'access_key' ] aliases: [ 'ec2_access_key', 'access_key' ]
wait: wait:
description: 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 required: false
default: "no" default: "no"
choices: [ "yes", "no" ] choices: [ "yes", "no" ]
@ -277,10 +277,18 @@ except ImportError:
print "failed=True msg='boto required for this module'" print "failed=True msg='boto required for this module'"
sys.exit(1) 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(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec = dict( 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), instance_name = dict(required=True),
source_instance = dict(required=False), 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), 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': elif command == 'promote':
required_vars = [ 'instance_name' ] 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' ] 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: for v in required_vars:
if not module.params.get(v): if not module.params.get(v):
@ -466,74 +482,108 @@ def main():
if new_instance_name: if new_instance_name:
params["new_instance_id"] = new_instance_name params["new_instance_id"] = new_instance_name
try: changed = True
if command == 'create':
db = conn.create_dbinstance(instance_name, size, instance_type, username, password, **params) if command in ['create', 'restore', 'facts']:
elif command == 'replicate': try:
if instance_type: result = conn.get_all_dbinstances(instance_name)[0]
params["instance_class"] = instance_type changed = False
db = conn.create_dbinstance_read_replica(instance_name, source_instance, **params) except boto.exception.BotoServerError, e:
elif command == 'delete': 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: if snapshot:
params["skip_final_snapshot"] = False params["skip_final_snapshot"] = False
params["final_snapshot_id"] = snapshot params["final_snapshot_id"] = snapshot
else: else:
params["skip_final_snapshot"] = True 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) if command == 'replicate':
elif command == 'modify': 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 params["apply_immediately"] = apply_immediately
db = conn.modify_dbinstance(instance_name, **params) result = conn.modify_dbinstance(instance_name, **params)
if apply_immediately: except boto.exception.BotoServerError, e:
if new_instance_name: module.fail_json(msg = e.error_message)
# Wait until the new instance name is valid if apply_immediately:
found = 0 if new_instance_name:
while found == 0: # Wait until the new instance name is valid
instances = conn.get_all_dbinstances() found = 0
for i in instances: while found == 0:
if i.id == new_instance_name: instances = conn.get_all_dbinstances()
instance_name = new_instance_name for i in instances:
found = 1 if i.id == new_instance_name:
if found == 0: instance_name = new_instance_name
time.sleep(5) found = 1
else: if found == 0:
# Wait for a few seconds since it takes a while for AWS time.sleep(5)
# to change the instance from 'available' to 'modifying' else:
time.sleep(5) # 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': if command == 'promote':
db = conn.promote_read_replica(instance_name, **params) try:
result = conn.promote_read_replica(instance_name, **params)
# Don't do anything for the 'facts' command since we'll just drop down except boto.exception.BotoServerError, e:
# to get_all_dbinstances below to collect the facts module.fail_json(msg = e.error_message)
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 # If we're not waiting for a delete to complete then we're all done
# so just return # so just return
if command == 'delete' and not wait: if command == 'delete' and not wait:
module.exit_json(changed=True) module.exit_json(changed=True)
try: try:
instances = conn.get_all_dbinstances(instance_name) resource = get_current_resource(conn, result.id, command)
my_inst = instances[0]
except boto.exception.BotoServerError, e: except boto.exception.BotoServerError, e:
module.fail_json(msg = e.error_message) module.fail_json(msg = e.error_message)
# Wait for the resource to be available if requested
# Wait for the instance to be available if requested
if wait: if wait:
try: try:
wait_timeout = time.time() + wait_timeout wait_timeout = time.time() + wait_timeout
time.sleep(5) 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) time.sleep(5)
if wait_timeout <= time.time(): if wait_timeout <= time.time():
module.fail_json(msg = "Timeout waiting for database instance %s" % instance_name) module.fail_json(msg = "Timeout waiting for resource %s" % resource.id)
instances = conn.get_all_dbinstances(instance_name) resource = get_current_resource(conn, result.id, command)
my_inst = instances[0]
except boto.exception.BotoServerError, e: except boto.exception.BotoServerError, e:
# If we're waiting for an instance to be deleted then # If we're waiting for an instance to be deleted then
# get_all_dbinstances will eventually throw a # 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 # If we got here then pack up all the instance details to send
# back to ansible # 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 = { d = {
'id' : my_inst.id, 'id' : resource.id,
'create_time' : my_inst.create_time, 'create_time' : resource.create_time,
'status' : my_inst.status, 'status' : resource.status,
'availability_zone' : my_inst.availability_zone, 'availability_zone' : resource.availability_zone,
'backup_retention' : my_inst.backup_retention_period, 'backup_retention' : resource.backup_retention_period,
'backup_window' : my_inst.preferred_backup_window, 'backup_window' : resource.preferred_backup_window,
'maintenance_window' : my_inst.preferred_maintenance_window, 'maintenance_window' : resource.preferred_maintenance_window,
'multi_zone' : my_inst.multi_az, 'multi_zone' : resource.multi_az,
'instance_type' : my_inst.instance_class, 'instance_type' : resource.instance_class,
'username' : my_inst.master_username, 'username' : resource.master_username,
'iops' : my_inst.iops 'iops' : resource.iops
} }
# Endpoint exists only if the instance is available # Endpoint exists only if the instance is available
if my_inst.status == 'available': if resource.status == 'available' and command != 'snapshot':
d["endpoint"] = my_inst.endpoint[0] d["endpoint"] = resource.endpoint[0]
d["port"] = my_inst.endpoint[1] d["port"] = resource.endpoint[1]
else: else:
d["endpoint"] = None d["endpoint"] = None
d["port"] = None d["port"] = None
# ReadReplicaSourceDBInstanceIdentifier may or may not exist # ReadReplicaSourceDBInstanceIdentifier may or may not exist
try: try:
d["replication_source"] = my_inst.ReadReplicaSourceDBInstanceIdentifier d["replication_source"] = resource.ReadReplicaSourceDBInstanceIdentifier
except Exception, e: except Exception, e:
d["replication_source"] = None d["replication_source"] = None
module.exit_json(changed=True, instance=d) module.exit_json(changed=changed, instance=d)
# import module snippets # import module snippets
from ansible.module_utils.basic import * from ansible.module_utils.basic import *