380 lines
17 KiB
Python
Executable File
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)
|