123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- #!/usr/bin/env python3
- #
- # Startup/Shutdown Groups of our systems
- #
- # Common usage:
- #
- # View the current state of the things on a schedule:
- # xdrtest status
- #
- # At the start of your shift:
- # xdrtest start --profile mdr-test-c2-gov
- #
- # At the end of your shift:
- # xdrtest stop
- #
- # You can also start/stop individual instances by ID (provided the tag _also_ matches):
- # xdrtest start openvpn
- import argparse
- import boto3
- import fnmatch
- import json
- import logging
- import os
- import re
- import sys
- DEBUG=False # Turn on with --debug
- logger = logging.getLogger()
- def query_yes_no(question, default="yes"):
- """Ask a yes/no question via input() and return their answer.
- "question" is a string that is presented to the user.
- "default" is the presumed answer if the user just hits <Enter>.
- It must be "yes" (the default), "no" or None (meaning
- an answer is required of the user).
- The "answer" return value is True for "yes" or False for "no".
- """
- valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False}
- if default is None:
- prompt = " [y/n] "
- elif default == "yes":
- prompt = " [Y/n] "
- elif default == "no":
- prompt = " [y/N] "
- else:
- raise ValueError("invalid default answer: '%s'" % default)
- while True:
- sys.stdout.write(question + prompt)
- choice = input().lower()
- if default is not None and choice == "":
- return valid[default]
- elif choice in valid:
- return valid[choice]
- else:
- sys.stdout.write("Please respond with 'yes' or 'no' " "(or 'y' or 'n').\n")
- def gather_profiles(file='~/.aws/config', commercial=False):
- file=os.path.expanduser(file)
- profiles=list()
- try:
- with open(os.path.expanduser(file), "r") as configfile:
- for line in configfile:
- match = re.search('^\[profile (.*)\]', line)
- if match:
- if 'test' in match.group(1):
- if 'gov' in match.group(1) or commercial:
- profiles.append(match.group(1))
- except Exception as e:
- logger.error(f'Could not open file {file} for reading: {e}')
- sys.exit(1)
- return profiles
- def gather_instances(ec2, tagname, tagvalue, exclude_state):
- '''
- Gathers the instance information from the provided ec2 resource
- '''
- desired_instances = ec2.instances.filter(
- Filters=[
- {
- 'Name': f'tag:{tagname}',
- 'Values': [ f'{tagvalue}' ]
- }
- ]
- )
- for i in desired_instances:
- name=""
- for t in i.tags:
- if t['Key']=='Name':
- name = t['Value']
- logger.debug(f'DEBUG: Discovered instance {i.instance_id}, Name: {name}, State: {i.state["Name"]}')
- if i.state['Name'] != exclude_state:
- # Why an iterator? Why not?
- yield {
- 'instance_id': i.instance_id,
- 'name': name,
- 'state': i.state['Name']
- }
- if __name__ == "__main__":
- handler = logging.StreamHandler()
- logger.addHandler(handler)
- # Turn off debugging for AWS
- for module in [ 'boto3', 'botocore', 'nose', 's3transfer', 'urllib3', 'urllib3.connectionpool' ]:
- l = logging.getLogger(module)
- l.setLevel(logging.WARNING) # bump this up if you think you have a boto3 bug
- parser = argparse.ArgumentParser(description='Startup/Shutdown Groups of Instances')
- parser.add_argument('action', help='Action to Take on Instances', choices=['stop', 'start', 'status'])
- parser.add_argument('--config', help='AWS Config File', default='~/.aws/config')
- parser.add_argument('--profile', help='Profile to Use', default=None)
- parser.add_argument('--tagname', help='Tag Name to Filter On', default='Schedule')
- parser.add_argument('--tagvalue', help='Tag Value to Filter On', default='MSOC')
- parser.add_argument('instancename', help='One or more instance names or IDs to action on. Wildcards are allowed.', nargs='*', default=None)
- parser.add_argument('--commercial', help='Include Commercial Accounts', action='store_true')
- parser.add_argument('--dry-run', help='Dry Run', action='store_true')
- parser.add_argument('--debug', help='Output Debug Information', action='store_true')
- args = parser.parse_args()
- if args.debug:
- DEBUG=True
- logger.setLevel(logging.DEBUG) # Debug
- else:
- logger.setLevel(logging.INFO) # Info
- if args.profile is not None:
- profiles=[ args.profile ]
- else:
- profiles=gather_profiles(file=args.config, commercial=args.commercial)
- logger.debug(f'Profiles to gather: {json.dumps(profiles)}')
- # Connect to our profiles:
- session = dict()
- ec2 = dict()
- instances = dict()
- instance_count = 0
- print(f'Instances to { args.action }:')
- for p in profiles:
- logger.debug(f'Gathering from profile {p}...')
- try:
- session[p] = boto3.session.Session(profile_name=p)
- ec2[p] = session[p].resource('ec2')
- except Exception as e:
- logger.error(f'Could not connect to profile "{p}": {e}')
- exclude_state = ""
- if args.action=="start":
- exclude_state="running"
- elif args.action=="stop":
- exclude_state="stopped"
-
- instances[p] = list()
- try:
- for instance in gather_instances(ec2[p], args.tagname, args.tagvalue, exclude_state):
- if args.instancename:
- # At least one instance name was specified, so go through and match
- for i in args.instancename:
- if fnmatch.fnmatch(instance['name'], i):
- print(f'{p:<30}\t{ instance["instance_id"] }\t{instance["state"]:<10}\t{instance["name"]}')
- instances[p].append(instance['instance_id'])
- instance_count += 1
- else:
- # No instance name specified, we add everything
- print(f'{p:<30}\t{ instance["instance_id"] }\t{instance["state"]:<10}\t{instance["name"]}')
- instances[p].append(instance['instance_id'])
- instance_count += 1
- except Exception as e:
- logger.error(f'Could not enumerate instances in profile "{p}": {e}')
- profiles.remove(p)
- if instance_count == 0:
- print('')
- print('No applicable instances discovered.')
- sys.exit(0)
- logger.debug(f'Num instances: {len(instances)}: {json.dumps(instances)}')
- if args.action=="status":
- logger.debug('Exiting after status check')
- sys.exit(0)
- print('')
- if query_yes_no(question='Continue?', default='no') is False:
- logger.debug('Exiting on user request')
- sys.exit(2)
- for p in profiles:
- if len(instances[p]) > 0:
- for i in ec2[p].instances.filter(InstanceIds=instances[p]):
- if args.action == 'start':
- print(f'Starting instances in profile { p }: { i.instance_id }')
- try:
- i.start(DryRun=args.dry_run)
- except Exception as e:
- print(f'An error occured while starting instance {i.id}. Error: {e}. Skipping.')
- elif args.action == 'stop':
- print(f'Stopping instances in profile { p }: { i.instance_id }')
- try:
- i.stop(DryRun=args.dry_run)
- except Exception as e:
- print(f'An error occured while stopping instance {i.id}. Error: {e}. Skipping.')
- else:
- print(f'Unsupported action: {args.action}')
- sys.exit(3)
|