#!/usr/bin/env python3 import json,lzma,os,subprocess,sys,time from zopen import zopen import botocore.session # if you have installed boto3 properly then you already have botocore #-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.------- main_region = 'us-east-1' compact = (',',':') ls = ['ls','-dGl','--full-time'] r = 'region' base_dir = 'attic' # relative to home directory ami_json = 'amijson' #-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.------- # these functions make code look less cluttered where isinstance() is needed. #-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.------- from decimal import Decimal from io import IOBase def is_bb (x):return isinstance(x, (bytes,bytearray) ) def is_bi (x):return isinstance(x, (bool,int) ) def is_bif (x):return isinstance(x, (bool,int,float) ) def is_bifc (x):return isinstance(x, (bool,int,float,complex) ) def is_bifcd (x):return isinstance(x, (bool,int,float,complex,Decimal) ) def is_bool (x):return isinstance(x, bool ) def is_bytearray (x):return isinstance(x, bytearray ) def is_bytes (x):return isinstance(x, bytes ) def is_complex (x):return isinstance(x, complex) def is_decimal (x):return isinstance(x, Decimal ) def is_dict (x):return isinstance(x, dict ) def is_ds (x):return isinstance(x, (dict,set) ) def is_fc (x):return isinstance(x, (float,complex) ) def is_fcd (x):return isinstance(x, (float,complex,Decimal) ) def is_fd (x):return isinstance(x, (float,Decimal) ) def is_float (x):return isinstance(x, float ) def is_frozenset (x):return isinstance(x, frozenset ) def is_if (x):return isinstance(x, (int,float) ) def is_ifc (x):return isinstance(x, (int,float,complex) ) def is_ifcd (x):return isinstance(x, (int,float,complex,Decimal) ) def is_ifd (x):return isinstance(x, (int,float,Decimal) ) def is_int (x):return isinstance(x, int ) def is_iobase (x):return isinstance(x, IOBase ) def is_lio (x):return isinstance(x, (list,IOBase) ) def is_list (x):return isinstance(x, list ) def is_ls (x):return isinstance(x, (list,set) ) def is_lt (x):return isinstance(x, (list,tuple) ) def is_ltsf (x):return isinstance(x, (list,tuple,set,frozenset) ) def is_ltsfd (x):return isinstance(x, (list,tuple,set,frozenset,dict) ) def is_sbb (x):return isinstance(x, (str,bytes,bytearray) ) def is_set (x):return isinstance(x, set ) def is_sf (x):return isinstance(x, (set,frozenset) ) def is_si (x):return isinstance(x, (str,int) ) def is_str (x):return isinstance(x, str ) def is_tf (x):return isinstance(x, (tuple,frozenset) ) def is_tuple (x):return isinstance(x, tuple ) #-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.------- #-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.------- home_dir = os.path.expanduser('~') try: os.chdir(home_dir) except OSError: exit('unable to change to home directory {home_dir!r}') try: os.chdir(base_dir) except OSError: exit('unable to change to base directory {base_dir!r}') #-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.------- # create a list of possible names to guess the name of the user preferred regions file #-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.------- fns = [] for a in ('-','_','.',''): for b in ('',f'my{a}'): for c in ('',f'pre{a}',f'pref{a}',f'prefer{a}',f'preferred{a}'): for d in ('',f'aws{a}'): for e in ('region','regions'): for f in ('','.txt','.reg','.regs','.list'): for g in ('','.gz','.bz2','.xz'): fns.append(b+c+d+e+f+g) #-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.------- cwd = os.getcwd() exe = sys.argv.pop(0) ses = botocore.session.get_session() w,h = os.get_terminal_size() errors = 0 def eprint(*args,**kwargs): kwargs['file'] = sys.stderr kwargs['flush'] = True result = False try: result = print(*args,**kwargs) except BrokenPipeError: pass return result def get_aws_regions_list(region=None): if region is None: region = main_region session = ses if not session: session = botocore.session.get_session() client = session.create_client('ec2', region_name=region) response = client.describe_regions() if 'Regions' in response: r = response['Regions'] if all('RegionName' in x for x in r): regions = [x['RegionName'] for x in r] return regions raise TypeError('AWS sent a bad region object: one or more regions had no name.') raise TypeError('AWS sent a bad response: no region object.') def by_creation_date(a): """Key function for sorting AMI items by creation date.""" return a.get('CreationDate','0') eprint('searching for a local file holding the user preferred list of regions') eprint(f'there are {len(fns)} such file names to try:') all_regions = [] line_end = '\n' pos = 0 num = 0 for fn in fns: if not fn: continue afn = os.path.join(home_dir,fn) new = len(fn) if pos+1+new > w: # print this name on the next line print('\n'+fn,end='',flush=True) pos = new num = 1 else: # print this name on the same line if pos: print('',fn,end='',flush=True) else: print(fn,end='',flush=True) pos += 1+new num += 1 if os.path.lexists(afn): with zopen(afn) as f: #-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.------- # 4 different ways to do this # the first 2 fail improperly due to iterator expecting to get bytes type from text file #-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.------- # all_regions = [x.strip()for x in f] #-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.------- # all_regions = [] # for x in f: # all_regions.append(x.strip()) #-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.------- all_regions = f.read().splitlines() #-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.------- # all_regions = [] # for x in f.readlines(): # all_regions.append(x.strip()) #-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.-------.------- n = len(all_regions) if not n: # ignore empty lists as if the do not exist continue print(f'\n\nfound one at {fn!r} with {n} regions listed in it.\n',flush=True) break else: eprint('\n\n>>> no file with a list of regions was found\n>>> getting a list from AWS region',main_region,flush=True) x2 = time.time() all_regions = get_aws_regions_list(main_region) x3 = time.time() eprint('duration =',x3-x2) n = len(all_regions) eprint(n,'regions') if not all_regions: exit('\nunable to get a list of preferred or official regions,\nnot even from AWS ... giving up\n') # determine what the directory name is for today if 'ami_json_ymd' not in locals(): ami_json_ymd = None if not is_str(ami_json_ymd): ami_json_ymd = ami_json + '-%Y-%m-%d.d' if '%' in ami_json_ymd: ami_json_ymd = time.strftime(ami_json_ymd,time.localtime(time.time())) if os.path.lexists(ami_json_ymd): exit(f'directory {ami_json_ymd!r} already exists - remove it or rename it and try again') print(f'CREATING DIRECTORY {ami_json_ymd!r}',flush=1) try: os.makedirs(ami_json_ymd) except OSError: exit('unknown error creating directory {ami_json_ymd!r}') try: os.chdir(ami_json_ymd) except FileNotFoundError: exit('directory {ami_json_ymd!r} was not created') except OSError: exit('cannot change currect working directory to {ami_json_ymd!r}') # the command line may have a list of regions use_regions = [] for arg in sys.argv: if arg in all_regions: use_regions.append(arg) else: eprint(f'Invalid region name {arg!r}') errors += 1 if errors: exit(f'aborting due to {errors} error{"s"[errors==1:]}') if not use_regions: # if no regions specified use_regions[:] = all_regions # then do all regions print('all_regions =',repr(all_regions),flush=1) n = len(all_regions) eprint(f'now fetching AMI data from {n} regions') c = 0 print('use_regions =',repr(use_regions),flush=1) for region in sorted(use_regions): c += 1 eprint(f'region {c}/{n} is {region!r}') print('region =',repr(region),flush=1) x0 = time.time() result = ses.create_client('ec2',region_name=region).describe_images(DryRun=False) x1 = time.time() eprint('duration =',x1-x0) if not result or 'Images' not in result: eprint('request "describe_images" failed for region',region) continue images = result['Images'] amis = [] for image in images: if image.get('ImageType',None)=='machine': amis.append(image) amis.sort(key=by_creation_date) # dump data for this region as compressed JSON fn = f'aws-amis-{region}.json.xz' eprint(f'in {os.getcwd()!r} dumping data to {fn!r}',flush=True) x4 = time.time() fz = lzma.open(fn,'wt',preset=9) json.dump(amis,fz,sort_keys=True,separators=compact,indent=4) fz.close() x5 = time.time() eprint('duration =',x5-x4) subprocess.call(ls+[fn]) # let "ls" show the file tn = ami_json + '.' + str(int(time.time()*3906250)) os.chdir(home_dir) os.chdir(base_dir) os.symlink(ami_json_ymd,tn) os.rename(tn,ami_json)