Skip to content

Commit

Permalink
Adding cf_stack.py for listing / modifying CF Stacks with given param…
Browse files Browse the repository at this point in the history
…eters
  • Loading branch information
haggaret committed Jan 11, 2017
1 parent f4f5c3c commit 9279a6d
Show file tree
Hide file tree
Showing 2 changed files with 306 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var/
*.egg-info/
.installed.cfg
*.egg
.idea/

# PyInstaller
# Usually these files are written by a python script from a template
Expand Down
305 changes: 305 additions & 0 deletions maestro/aws/cf_stack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
"""
cf_stack.py
CloudFormation Stack help methods - currently extending boto3
Note:
Credentials are required to communicate with AWS.
Set the following ENVIRONMENT VARIABLES to appropriate values
before running this script:
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
"""

import sys
import boto3, botocore
import argparse
import logging

regions = ['us-east-1', 'us-east-2', 'us-west-1', 'us-west-2', 'eu-west-1', 'eu-west-2']


def _log_and_print_to_console(msg, log_level='info'):
"""
Print a message to the console and log it to a file
:param msg: the message to print and log
:param log_level: the logging level for the mesage
"""
log_func = {'info': logging.info, 'warn': logging.warn, 'error': logging.error}
print(msg)
log_func[log_level.lower()](msg)


def _update_stack_parameters(region, stack_id, parameters, dryrun=False):
"""
Update a given stack with the given parameters
:param region: region that the stack exists in
:param stack_id: the name or ID of the stack
:param parameters: list of parameter objects
:param dryrun: if true, no changes are made
:return: json object
"""
if not region:
_log_and_print_to_console("ERROR: You must supply a region to scan", 'error')
return None
else:
_log_and_print_to_console('Updating Stack: ' + stack_id)
for param in parameters:
if 'PreviousValue' in param:
_log_and_print_to_console(' ' + param['ParameterKey'])
_log_and_print_to_console(' OLD: ' + param['PreviousValue'])
_log_and_print_to_console(' NEW: ' + param['ParameterValue'])
del param['PreviousValue']
if not dryrun:
stack = get_stack_with_name_or_id(region, stack_id)
cf_client = boto3.client('cloudformation', region)
try:
if 'Capabilities' in stack:
status = cf_client.update_stack(StackName=stack_id, Parameters=parameters, UsePreviousTemplate=True, Capabilities=stack['Capabilities'])['StackId']
else:
status = cf_client.update_stack(StackName=stack_id, Parameters=parameters, UsePreviousTemplate=True)['StackId']
return status
except botocore.exceptions.ClientError as e:
if e.response['Error']['Code'] == 'ValidationError' and 'No updates are to be performed' in \
e.response['Error']['Message']:
_log_and_print_to_console(" ERROR: New value matches Old value - no update required", 'error')
else:
_log_and_print_to_console('Unexpected error: %s' % e)
return False


def get_stacks_with_given_parameter(region, parameter_list):
"""
Get a list of stacks that have at least one parameter with a name in the given list
:param region: the region to scan
:param parameter_list: list of possible parameter names to look for
:return: list of stacks that have a parameter with a name in the given list
"""
stacks_with_given_parameter = []
if not region:
_log_and_print_to_console("ERROR: You must supply a region to scan", 'error')
return None
else:
cf_client = boto3.client('cloudformation', region)
cf_data = cf_client.describe_stacks()
if "Stacks" in cf_data:
for stack in cf_data["Stacks"]:
if "Parameters" in stack:
for parameter in stack["Parameters"]:
if parameter['ParameterKey'] in parameter_list:
logging.debug(
"Found parameter - " + parameter['ParameterKey'] + ' - in stack: ' + stack['StackName'])
stacks_with_given_parameter.append(stack)
break
return stacks_with_given_parameter


def get_stack_with_name_or_id(region, stack_id):
"""
Get the stack with the given name or ID
:param region: region where the stack exists
:param stack_id: stack name or ID
:return: stack in question or empty dictionary
"""
stack = {}
if not region:
_log_and_print_to_console("ERROR: You must supply a region to scan", 'error')
return None
else:
cf_client = boto3.client('cloudformation', region)
cf_data = cf_client.describe_stacks(StackName=stack_id)
logging.debug("Getting stack description for stack with name/id: " + stack_id)
if "Stacks" in cf_data:
if len(cf_data["Stacks"]) > 1:
# Problem - mutliple stacks with this name??
_log_and_print_to_console("Error: Multiple stacks with given name", 'error')
else:
stack = cf_data['Stacks'][0]
return stack


def get_new_parameter_list_for_update(stack, expected_value, new_value, parameter_to_change, force=False):
"""
Given a list of possible parameter names, generate a new parameter list with
existing_value changed to new_value
:param stack: the stack to mine the parameter list from
:param expected_value: expected existing value for the parameter
:param new_value: the new value to use for the parameter
:param parameter_to_change: list containing possible parameter names
:return: tuple containing a boolean for whether an update is required and the new parameter list
"""
new_parameters_list = []
update_required = False
for parameter in stack['Parameters']:
new_param = {}
new_param['ParameterKey'] = parameter['ParameterKey']
if parameter['ParameterKey'] in parameter_to_change:
if force or (expected_value in parameter['ParameterValue']):
update_required = True
new_param['ParameterValue'] = new_value
new_param['PreviousValue'] = parameter['ParameterValue']
else:
_log_and_print_to_console(
"Unexpected value detected - Stack will NOT be updated\n Stack: " + stack[
'StackName'] + "\n Existing value: " + parameter['ParameterValue'], 'warn')
new_param['UsePreviousValue'] = True
else:
new_param['UsePreviousValue'] = True
new_parameters_list.append(new_param)
return update_required, new_parameters_list


def list_stacks_with_given_parameter(region, parameter_list):
"""
Print a list of stacks in a given region that contain at least one parameter in the given parameter list
:param region: region to scan
:param parameter_list: list of possible parameter names
"""
if not region:
_log_and_print_to_console("ERROR: You must supply a region to scan", 'error')
else:
_log_and_print_to_console(
"\nCloudFormation Stacks in region: " + region + " with at least one of the following parameters: " + ', '.join(
parameter_list))
stacks_with_given_parameter = get_stacks_with_given_parameter(region, parameter_list)
if len(stacks_with_given_parameter) > 0:
for stack in stacks_with_given_parameter:
for parameter in stack["Parameters"]:
if parameter['ParameterKey'] in parameter_list:
_log_and_print_to_console(
"Stack Name: " + stack["StackName"] + "\n " + parameter['ParameterKey'] + ": " +
parameter[
"ParameterValue"])
break
else:
_log_and_print_to_console("None")


def list_stacks_with_given_parameter_all_regions(parameter_list):
"""
Print a list of stacks for all regions that contain at least one parameter in the given parameter list
:param parameter_list: list of possible parameter names
"""
for region in regions:
list_stacks_with_given_parameter(region, parameter_list)


def update_stack_with_given_parameter(region, stack_name, expected_value, new_value, parameter_to_change, dryrun=False,
force=False):
"""
Update the given parameter in the given stack (for the given region) to a new value, provided
the existing value contains the expected_value
:param region: region where the stack exists
:param stack_name: the stack name or ID
:param expected_value: the existing value of the parameter should contain the expected_value
:param new_value: the new value for the parameter
:param parameter_to_change: list of possible names for the parameter
:param dryrun: if true, no changes are made
:returns: True / False
"""
stack = get_stack_with_name_or_id(region, stack_name)
update_required, new_parameter_list = get_new_parameter_list_for_update(stack, expected_value, new_value,
parameter_to_change, force)
if update_required:
return _update_stack_parameters(region, stack["StackId"], new_parameter_list, dryrun)


def update_all_stacks_with_given_parameter(region, expected_value, new_value, parameter_to_change, dryrun=False,
force=False):
"""
Update the given parameter in all stacks (for the given region) to a new value, provided
the existing value contains the expected_value
:param region: region where the stacks exist
:param expected_value: the existing value of the parameter should contain the expected_value
:param new_value: the new value for the parameter
:param parameter_to_change: list of possible names for the parameter
:param dryrun: if true, no changes are made
"""
_log_and_print_to_console('\nUpdating all matching Stacks in region: ' + region)
update_status = {}
for stack in get_stacks_with_given_parameter(region, parameter_to_change):
update_required, new_parameter_list = get_new_parameter_list_for_update(stack, expected_value, new_value,
parameter_to_change, force)
if update_required:
update_status[stack['StackId']] = _update_stack_parameters(region, stack["StackId"], new_parameter_list,
dryrun)

return update_status


if __name__ == "__main__":

logging.basicConfig(filename='update_cf_stack_param.log', format='%(asctime)s - %(levelname)7s : %(message)s',
level=logging.INFO)

parser = argparse.ArgumentParser(
description='Script to view/modify a Parameter in CloudFormation Stacks')

parser.add_argument("--verbose", help="Turn on DEBUG logging", action='store_true', required=False)
parser.add_argument("--param", help="space separated list of possible names for the parameter", dest='param',
nargs='+', required=True)
parser.add_argument("--list",
help="List all stacks in a given region that have a given parameter",
dest='list', action='store_true', required=False)
parser.add_argument("--region", help="Specify a region to use (all is valid)", dest='region', required=True)
parser.add_argument("--update",
help="Update Parameter to new value for given stack in the specified region - must supply expected existing value and new value. STACK_ID can be the name or ID of the Stack",
dest='update', nargs=3, metavar=('STACK_ID', 'EXPECTED_VALUE', 'NEW_VALUE'), required=False)
parser.add_argument("--update-all",
help="Update Parameter to new value for all stacks in the specified region - must supply expected existing value and new value",
dest='update_all', nargs=2, metavar=('EXPECTED_VALUE', 'NEW_VALUE'), required=False)
parser.add_argument("--dryrun", help="Do a dryrun - no changes will be performed", dest='dryrun',
action='store_true', default=False, required=False)
parser.add_argument("--force", help="Force an update, even if expected value doesn't match", dest='force',
action='store_true', default=False, required=False)
args = parser.parse_args()

log_level = logging.INFO

if args.verbose:
print("Verbose logging selected")
log_level = logging.DEBUG

logging.info("INIT")

print('')

if args.dryrun:
_log_and_print_to_console("***** Dryrun selected - no changes will be made *****\n")

_log_and_print_to_console("Searching for Stacks with a Parameter with name(s): " + ' '.join(args.param))

if args.region:
_log_and_print_to_console("Region: " + args.region)

if args.list:
if args.region is "all":
list_stacks_with_given_parameter_all_regions(args.param)
else:
list_stacks_with_given_parameter(args.region, args.param)
if args.update:
if 'all' in args.region:
_log_and_print_to_console(
"Cannot specify a region value of all with update, please use --region to specify a region")
sys.exit(1)
update_status = update_stack_with_given_parameter(args.region, args.update[0], args.update[1], args.update[2],
args.param, args.dryrun, args.force)
if not args.dryrun:
if update_status:
_log_and_print_to_console("\nStack Parameter Update Succeeded")
else:
_log_and_print_to_console("\nStack Parameter Update Failed")

if args.update_all:
if 'all' in args.region:
_log_and_print_to_console(
"Cannot specify a region value of all with update, please use --region to specify a region")
sys.exit(1)
update_status = update_all_stacks_with_given_parameter(args.region, args.update_all[0], args.update_all[1],
args.param, args.dryrun, args.force)
if not args.dryrun and update_status:
_log_and_print_to_console("\n\nUPDATE STATUS:")
for stack in update_status:
_log_and_print_to_console(stack + ": " + ('Succeeded' if update_status[stack] else 'Failed'))

_log_and_print_to_console('\nCOMPLETE')

0 comments on commit 9279a6d

Please sign in to comment.