aws-missing-tools/ec2-modify-ebs-volume/ec2-modify-ebs-volume.py

380 lines
17 KiB
Python
Executable File

#!/usr/bin/env python
# Author: Colin Johnson / colin@cloudavail.com
# Date: 2013-11-17
# Version 0.1
# License Type: GNU GENERAL PUBLIC LICENSE, Version 3
import argparse
import logging
import os
import time
from boto import ec2
from boto import exception
app_name = os.path.basename(__file__)
unix_time_current = time.time()
# datetime_format_standard is ISO 8601 combined date and time with timezone
datetime_format_standard = '%Y-%m-%dT%H:%M:%S%z'
datetime_current = time.strftime(datetime_format_standard,
time.gmtime(unix_time_current))
def attach_volume(instance_object, volume_object, device):
''' given a ec2.instance.Instance object, an ec2.volume.Volume object and
a device, attach the volume object and wait for the volume to show as
in-use '''
logging.debug('attach_volume called.')
attach_result = volume_object.attach(instance_object.id, device)
logging.info('attach result was: {!s}'.format(attach_result))
wait_aws_event(object_in=volume_object, object_type='volume',
wait_for_string='in-use')
def create_snapshot(volume_object, snapshot_description=None):
''' given a volume object, take a snapshot and return a snapshot object '''
logging.debug('create_snapshot called.')
snapshot_object = volume_object.create_snapshot(snapshot_description)
wait_aws_event(object_in=snapshot_object, object_type='snapshot',
wait_for_string='completed')
return snapshot_object
def create_volume(snapshot_object, size, availability_zone, iops=None,
volume_type=None):
''' given a boto.ec2.snapshot.Snapshot, size and availability zone, returns
a volume object '''
logging.debug('create_volume called.')
logging.info('snapshot_id: {!s}, size: {!s}, availability_zone: {!s}, iops: {!s}, volume_type: {!s}'
.format(snapshot_object, size, availability_zone, iops, volume_type))
volume_object = snapshot_object.create_volume(availability_zone, size=size,
iops=iops,
volume_type=volume_type)
wait_aws_event(object_in=volume_object, object_type='volume',
wait_for_string='available')
logging.info('new volume id is: {!s}'.format(volume_object.id))
return volume_object
def detach_volume(volume_object):
''' given a boto.ec2.volume.Volume object, detaches it and waits for the
volume to show as available'''
logging.debug('detach_volume called.')
try:
detach_result = volume_object.detach()
except exception.EC2ResponseError:
logging.critical('An error occured when attempting to detach volume {!s}.'.format(volume_object.id))
exit (1)
logging.info('detach result was: {!s}'.format(detach_result))
wait_aws_event(object_in=volume_object, object_type='volume',
wait_for_string='available')
def get_block_device_type(block_device_name, block_device_mapping):
''' given a block device name and a block device mapping, returns a
boto.ec2.blockdevicemapping.BlockDeviceType object'''
block_device_type = None
logging.debug('get_block_device_type called.')
logging.info('device is {!s}.'.format(block_device_name))
if block_device_name in block_device_mapping:
logging.info('block device {!s} found'.format(block_device_name))
block_device_type = block_device_mapping[block_device_name]
else:
logging.critical('block device {!s} not found.'
.format(block_device_name))
exit(1)
logging.info('block_device_name\'s volume_id is: {!s}.'
.format(block_device_type.volume_id))
return block_device_type
def get_block_device_volume(ec2_connection, volume_id):
''' given an ec2_connection object and a volume_id, returns a volume
object'''
logging.debug('get_block_device_volume called.')
volume_object = None
get_all_volumes_result = ec2_connection.get_all_volumes(volume_ids=[volume_id])
volume_object = get_all_volumes_result[0]
return volume_object
def get_selected_instances(instance_id):
''' given an instance_id returns an instance object '''
logging.debug('get_selected_instances called.')
try:
instances = ec2_connection.get_only_instances([instance_id])
except exception.EC2ResponseError:
logging.critical('Unable to get selected instance due to an EC2ResponseError.')
exit(1)
if len(instances) == 0:
exit(1)
instance = instances[0]
logging.info('instance_id found: {!s}'.format(instance.id))
return instance
def return_desired_volume_size(aws_limits, args, volume_object):
# determine volume_attributes['size']
desired_volume_size = None
if args.volume_size is not None:
if args.volume_size > aws_limits['max_volume_size']:
logging.critical('--volume-size can not be greater than {!s}. You requested --volume-size {!s}.'
.format(aws_limits['max_volume_size'],
args.volume_size))
exit(1)
else:
desired_volume_size = args.volume_size
else:
desired_volume_size = volume_object.size
# validate volume_attributes['size']
# the desired_volume_size must be greater than the previous_volume.size
if volume_object.size > desired_volume_size:
logging.critical('--volume-size must be greater than the existing volume size. You requested --volume-size {!s} and volume {!s} has a size of {!s}.'
.format(desired_volume_size, volume_object.id,
volume_object.size))
exit(1)
logging.info('desired volume size will be: {!s}'
.format(desired_volume_size))
return desired_volume_size
def return_desired_iops(aws_limits, args, volume_object, volume_size):
desired_iops = None
# handle the condition where the user has input --volume-type io1 and
# not input an --iops value
if args.iops is None:
if args.volume_type is not None:
logging.critical('if a --volume-type except standard is specified --iops <int> must be specified as well.')
exit(1)
else:
desired_iops = volume_object.iops
else:
desired_iops = args.iops
if desired_iops < aws_limits['min_iops']:
logging.critical('--iops must be greater than {!s}.'.format(aws_limits['min_iops']))
exit(1)
elif desired_iops > aws_limits['max_iops']:
logging.critical('--iops must be less than {!s}.'.format(aws_limits['max_iops']))
exit(1)
# validate volume_attributes['iops'] settings with volume_attributes['size']
# http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html#EBSVolumeTypes_piops
max_allowed_iops = (aws_limits['max_iops_size_multiplier'] * volume_size)
if desired_iops > max_allowed_iops:
logging.critical('--iops may not be greater than {!s} times volume size. Maximum allowable iops is {!s}.'
.format(aws_limits['max_iops_size_multiplier'], max_allowed_iops))
exit(1)
logging.info('desired volume iops will be: {!s}'.format(desired_iops))
return desired_iops
def validate_standard_volume_attrs(args, volume_object, aws_limits):
if args.iops is not None:
logging.critical('--iops can only be used if --volume-type io1.')
exit(1)
if volume_object.type is not 'standard':
logging.warning('You requested standard and current volume type is {!s}.'
.format(volume_object.type))
def return_desired_volume_attrs(args, volume_object):
''' given an arguments object and a volume object returns sensible volume
attributes for a new volume. Rule is to always use argument if provided, else
to fall back to existing attributes else to use defaults.'''
logging.debug('return_desired_volume_attrs called.')
volume_attributes = {'size': None, 'volume_type': None, 'iops': None}
# max_iops_size_multiplier is used to determine the allowable number of iops
# given a volume size. iops can be no greater than 30 x volume size as of
# 2013-11-17
aws_limits = {'max_volume_size': 1024, 'min_iops': 100, 'max_iops': 4000,
'max_iops_size_multiplier': 30}
volume_attributes['size'] = return_desired_volume_size(aws_limits=aws_limits,
args=args,
volume_object=volume_object)
# determine volume_attributes['type']
if args.volume_type is not None:
volume_attributes['type'] = args.volume_type
else:
volume_attributes['type'] = volume_object.type
logging.info('desired volume type will be: {!s}'
.format(volume_attributes['type']))
if volume_attributes['type'] == 'standard':
validate_standard_volume_attrs(aws_limits=aws_limits, args=args, volume_object=volume_object)
elif volume_attributes['type'] == 'io1':
volume_attributes['iops'] = return_desired_iops(aws_limits=aws_limits,
args=args,
volume_object=volume_object,
volume_size=volume_attributes['size'])
else:
logging.critical('Supported --volume-types are \'io1\' and \'standard\'.')
exit(1)
return volume_attributes
def start_instance(instance_object):
logging.debug('start_instance called.')
instance_object.start()
wait_aws_event(object_in=instance_object, object_type='instance',
wait_for_string='running')
def stop_instance(instance_object):
logging.debug('stop_instance called.')
instance_object.stop()
wait_aws_event(object_in=instance_object, object_type='instance',
wait_for_string='stopped')
def wait_aws_event(object_in, object_type, wait_for_string):
''' given an object, a string describing the type of object and a string
that will be return when the resource represented by the object is available,
wait_aws_event will poll the object to determine if the resource is represents
is available for service. wait_aws_event returns when '''
logging.debug('wait_aws_event called.')
# allowed_wait_time reflects the number of seconds a that wait_aws_event
# will wait for a status or state change
allowed_wait_time = 900
# determines the correct attribute to poll to determine the state/status
# of an object. Instance resources/objects use 'state' while other
# resources/objects use status.
if object_type == 'instance':
attribute = 'state'
else:
attribute = 'status'
total_time_waiting = 0
time_between_polling = 5
object_id = getattr(object_in, 'id')
object_status = getattr(object_in, attribute)
logging.info('wait_aws_event will wait for {!s} seconds {!s} {!s} to return to {!s} {!s}.'
.format(allowed_wait_time, object_type, object_id, attribute,
wait_for_string))
while object_status != unicode(wait_for_string) and total_time_waiting < allowed_wait_time:
object_in.update()
object_status = getattr(object_in, attribute)
logging.info('{!s} {!s}\'s {!s} is {!s}. Time elapsed is {!s} seconds.'
.format(object_type, object_id, attribute, object_status,
total_time_waiting))
time.sleep(time_between_polling)
total_time_waiting += time_between_polling
logging.info('total time waiting for {!s} {!s} to return to {!s} {!s}: {!s} seconds.'
.format(object_type, object_id, attribute, wait_for_string,
total_time_waiting))
if object_status == unicode(wait_for_string):
pass
else:
logging.critical('{!s} {!s} did not return to {!s} {!s} in {!s} seconds.'
.format(object_type, object_id, attribute,
wait_for_string, allowed_wait_time))
exit(1)
return object_status
# creates ec2_connection object
try:
ec2_connection = ec2.connect_to_region('us-east-1')
except:
logging.critical('An error occured when attempting to connect to the AWS API.')
exit(1)
# aws_regions contains a list of strings representing AWS regions
aws_regions = [ (str(region.name)) for region in ec2_connection.get_all_regions() ]
parser = argparse.ArgumentParser()
parser.add_argument('--device', default='root',
help='select the device to modify by device attachment point. Example, /dev/sda1, /dev/sdf or root to select the root device.')
parser.add_argument('--instance-id', dest='instance_id', required=True,
help='set the instance-id of the instance that should have the EBS volume expanded.')
parser.add_argument('--iops', type=int, default=None,
help='set the number of iops the EBS volume should provide.')
parser.add_argument('--log-level', dest='log_level', default='INFO',
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
help='set the log level when running {!s}.'.format(app_name))
parser.add_argument('--region',
help='set the region where instances should be located.',
default='us-east-1', choices=aws_regions)
parser.add_argument('--volume-size', dest='volume_size', type=int, default=None,
help='set the volume size that the EBS volume should be.')
parser.add_argument('--volume-type', dest='volume_type', default=None,
choices=['standard', 'io1'],
help='set the type of EBS volume.')
args = parser.parse_args()
# configure logging
log_format = '%(message)s'
log_level = str.upper(args.log_level)
logging.basicConfig(level=log_level, format=log_format)
region = args.region
instance_id = args.instance_id
device = args.device
# gets an instance object corresponding to the instance_id
selected_instance = get_selected_instances(instance_id)
# if the given instance_id's shutdown behavior is not stop then exit
# if the instance_id's shutdown behavior is terminate than the EBS volume would
# be deleted
shutdown_behavior = ec2_connection.get_instance_attribute(instance_id, 'instanceInitiatedShutdownBehavior')
if shutdown_behavior[unicode('instanceInitiatedShutdownBehavior')] != unicode('stop'):
logging.critical('instance_id {!s}\'s shutdown behavior must be "stop."\
{!s}\'s shutdown behavior is "{!s}."'.format(instance_id, instance_id,
shutdown_behavior[unicode('instanceInitiatedShutdownBehavior')]))
exit(1)
instance_az = selected_instance.placement
instance_initial_state = selected_instance.state
logging.info('instance_id {!s}\'s state is {!s}.'
.format(selected_instance.id, instance_initial_state))
if device == 'root':
selected_device = selected_instance.root_device_name
else:
selected_device = device
logging.info('selected device is: {!s}'.format(selected_device))
instance_block_device_mapping = selected_instance.block_device_mapping
# given the device and mapping, get a BlockDeviceType object - this object
# represents the volume to be modified
block_device_type = get_block_device_type(selected_device,
instance_block_device_mapping)
# given a BlockDeviceType object, get the a volume object - this object
# represents the volume resource that will be increased in size
previous_volume = get_block_device_volume(ec2_connection,
block_device_type.volume_id)
desired_volume_attrs = return_desired_volume_attrs(args=args,
volume_object=previous_volume)
# stops the instance - this may not be required, but is recommended in
# http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-expand-volume.html#recognize-expanded-volume-linux
stop_instance(instance_object=selected_instance)
# detach the volume to be resized from the instance - this could be done after
# the snapshot is taken
detach_volume(volume_object=previous_volume)
snapshot_description = '{!s}-{!s}'.format(app_name, datetime_current)
previous_volume_snapshot = create_snapshot(volume_object=previous_volume,
snapshot_description=snapshot_description)
new_volume = create_volume(snapshot_object=previous_volume_snapshot,
availability_zone=instance_az,
size=desired_volume_attrs['size'],
iops=desired_volume_attrs['iops'],
volume_type=desired_volume_attrs['type'])
attach_volume(instance_object=selected_instance, volume_object=new_volume,
device=selected_device)
if instance_initial_state == unicode('running'):
logging.info('instance_id {!s}\'s state was {!s}. instance_id {!s} will be returned to running state.'
.format(selected_instance.id, instance_initial_state,
selected_instance.id))
start_instance(instance_object=selected_instance)