123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- #!/usr/bin/env python3
- """
- CLI tool to help with AMI sharing. What I was doing before in bash
- hit its reasonable limit of complexity, and was getting hard to read
- because it was in bash.
- Some notes:
- [1] Specifying an AMI filter is mandatory, but regions and accounts are not.
- [2] Standard AWS_PROFILE environment variables are supported (because boto3 supports)
- [3] AWS_REGION environment variable (as used by the AWS CLI) is not supported because
- boto3 does not use it. It uses AWS_DEFAULT_REGION instead. If your
- $HOME/.aws/config lists a region for a given profile this is not needed.
- Quick Common Usage:
- If you've added an XDR managed account (such as a customer slice account):
- AWS_PROFILE=mdr-common-services-gov update-ami-accounts MSOC* <account>
- If you've added a customer account (such as a place for their LCPs)
- AWS_PROFILE=mdr-common-services-gov update-ami-accounts TODO_DETERMINEWHATGOESHERE <account>
- Example 1: Let's just run a report of all AMIs matching '*Duane*' in all regions that the
- profile has access to. Notice the wildcards in quotes so bash won't try to expand them
- out to filenames.
- [duane.e.waddle@DPS0591 bin]$ AWS_PROFILE=gov-common-services-terraformer ./update-ami-accounts '*Duane*'
- Looking for AMIs matching "*Duane*" in the following regions:
- us-gov-east-1
- us-gov-west-1
- AMIs matching the filter:
- region |ami id |ami name
- ===============|======================|========================================
- us-gov-east-1 |ami-069f3e239427365b6 |Duane_Testing_20201124233617
- us-gov-west-1 |ami-0ee37a86b09aefad0 |Duane_Testing_20201124233617
- Example 2: Regions can be specified with a list or wildcard. This is just a report too:
- [duane.e.waddle@DPS0591 bin]$ AWS_PROFILE=gov-common-services-terraformer ./update-ami-accounts --region us-gov-east-1 --region '*west*' '*Duane*'
- Looking for AMIs matching "*Duane*" in the following regions:
- us-gov-east-1
- us-gov-west-1
- AMIs matching the filter:
- region |ami id |ami name
- ===============|======================|========================================
- us-gov-east-1 |ami-069f3e239427365b6 |Duane_Testing_20201124233617
- us-gov-west-1 |ami-0ee37a86b09aefad0 |Duane_Testing_20201124233617
- Example 3: If we list one or more accounts then sharing is updated
- [duane.e.waddle@DPS0591 bin]$ AWS_PROFILE=gov-common-services-terraformer ./update-ami-accounts --region '*1' '*Duane*' 738800754746 721817724804
- Looking for AMIs matching "*Duane*" in the following regions:
- us-gov-east-1
- us-gov-west-1
- Sharing AMIs with these accounts:
- 738800754746
- 721817724804
- AMIs matching the filter:
- region |ami id |ami name |status
- ===============|======================|========================================|==========
- us-gov-east-1 |ami-069f3e239427365b6 |Duane_Testing_20201124233617 |success
- us-gov-west-1 |ami-0ee37a86b09aefad0 |Duane_Testing_20201124233617 |success
- Example 4: Sharing updates are atomic so if you could get a failure because
- one of several accounts you listed does not exist:
- [duane.e.waddle@DPS0591 bin]$ AWS_PROFILE=gov-common-services-terraformer ./update-ami-accounts --region '*1' '*Duane*' 738800754746 72181772480
- Looking for AMIs matching "*Duane*" in the following regions:
- us-gov-east-1
- us-gov-west-1
- Sharing AMIs with these accounts:
- 738800754746
- 72181772480
- AMIs matching the filter:
- region |ami id |ami name |status
- ===============|======================|========================================|==========
- us-gov-east-1 |ami-069f3e239427365b6 |Duane_Testing_20201124233617 |error
- us-gov-west-1 |ami-0ee37a86b09aefad0 |Duane_Testing_20201124233617 |error
- Notice one of the account numbers is missing a digit. You don't get a clear
- message as to which one caused the error. Maybe one day I'll improve that...
- """
- import argparse
- import re
- import sys
- import boto3
- import botocore
- from botocore.config import Config
- def list_matching_regions(region_filter):
- """
- Return the list of regions matching a wildcard.
- """
- ec2 = boto3.client('ec2')
- regions = ec2.describe_regions(
- Filters=[
- {
- 'Name': 'region-name',
- 'Values': [ region_filter ]
- }
- ],
- AllRegions = False
- )
- return [ x.get('RegionName') for x in regions.get('Regions',[]) ]
- def find_amis_matching_filter(ami_filter,region=None):
- """
- Return a list of AMIs matching a given wildcard by name
- """
- if region is not None:
- ec2 = boto3.client('ec2',config=Config(region_name=region))
- else:
- ec2 = boto3.client('ec2')
- images = ec2.describe_images(
- Owners=['self'],
- Filters=[
- {
- 'Name': 'name',
- 'Values': [ ami_filter ]
- }
- ]
- )
- fields_of_interest = { 'ImageId', 'Name' }
- for entry in images.get('Images',[]):
- newentry = { k: entry[k] for k in entry.keys() & fields_of_interest }
- yield newentry
- def share_ami(ami,region,accounts,remove=False):
- """
- Share a specific AMI (by id) with a list of AWS account IDs
- within a specific region
- """
- launchparam = { }
- launchparam['Add'] = []
- if not remove:
- for account in accounts:
- launchparam['Add'].extend([{'UserId': account}])
- else:
- for account in accounts:
- launchparam['Remove'].extend([{'UserId': account}])
- ec2 = boto3.resource('ec2',config=Config(region_name=region))
- # expecting to possibly raise something here
- image = ec2.Image(ami)
- image.modify_attribute(Attribute='launchPermission', LaunchPermission=launchparam)
- def runmain(ami_filter,accounts,region_filters,remove=False):
- """
- main
- """
- region_list = []
- if region_filters is None:
- region_list.extend(list_matching_regions('*'))
- else:
- for filt in region_filters:
- region_list.extend(list_matching_regions(filt))
- region_list=list(sorted(set(region_list)))
- print('Looking for AMIs matching "{0}" in the following regions:'.format(ami_filter))
- for region in region_list:
- print(" {0}".format(region))
- print("")
- # Shorter format for when we're just doing a report of matching AMIs
- # No accounts means just report mode
- if len(accounts) > 0:
- print("Sharing AMIs with these accounts:")
- for account in accounts:
- print(" {0}".format(account))
- print("")
- print("AMIs matching the filter:")
- report_format="{0:<15}|{1:<22}|{2:<40}|{3:<10}"
- print(report_format.format('region','ami id','ami name','status'))
- print(report_format.format('='*15,'='*22,'='*40,'='*10))
- else:
- print("AMIs matching the filter:")
- report_format="{0:<15}|{1:<22}|{2:<40}"
- print(report_format.format('region','ami id','ami name'))
- print(report_format.format('='*15,'='*22,'='*40))
- for region in region_list:
- for ami in find_amis_matching_filter(ami_filter,region):
- if len(accounts) > 0:
- try:
- share_ami(ami.get('ImageId'),region,accounts,remove)
- print(report_format.format(region,ami.get('ImageId'),ami.get('Name'),"success"))
- except botocore.exceptions.ClientError:
- print(report_format.format(region,ami.get('ImageId'),ami.get('Name'),"error"))
- # No accounts we're just making a report ...
- else:
- print(report_format.format(region,ami.get('ImageId'),ami.get('Name')))
- def cli():
- parser = argparse.ArgumentParser()
- parser.add_argument('--region',action='append',required=False,
- help='Region to add sharing in (can specify multiple)')
- parser.add_argument('--remove',action='store_true',required=False,
- help='Set this flag to remove instead of add sharing.')
- parser.add_argument('ami_filter',help='AMI Filter to apply')
- parser.add_argument('accounts',nargs='*',help='list of AWS accounts to add AMIs to')
- args = parser.parse_args()
- # Remove dashes from account IDs as a nice thing for user
- args.accounts = [ f.replace("-","") for f in args.accounts ]
- # AWS Accounts are 12 digits, all digits
- invalid_accounts = []
- digit_check = re.compile(r"^\d+$")
- for account in args.accounts:
- if len(account) != 12:
- invalid_accounts.append("{0}: {1}".format(account,"Account length not 12"))
- elif digit_check.fullmatch(account) is None:
- invalid_accounts.append("{0}: {1}".format(account,"Account contains non-digit chars"))
- if len(invalid_accounts) > 0:
- for message in invalid_accounts:
- print(message)
- sys.exit(1)
- runmain(args.ami_filter,args.accounts,args.region,args.remove)
- if __name__ == "__main__":
- cli()
|