xdrtest 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. #!/usr/bin/env python3
  2. #
  3. # Startup/Shutdown Groups of our systems
  4. #
  5. # Common usage:
  6. #
  7. # View the current state of the things on a schedule:
  8. # xdrtest status
  9. #
  10. # At the start of your shift:
  11. # xdrtest start --profile mdr-test-c2-gov
  12. #
  13. # At the end of your shift:
  14. # xdrtest stop
  15. #
  16. # You can also start/stop individual instances by ID (provided the tag _also_ matches):
  17. # xdrtest start openvpn
  18. import argparse
  19. import boto3
  20. import json
  21. import logging
  22. import os
  23. import re
  24. import sys
  25. DEBUG=False # Turn on with --debug
  26. logger = logging.getLogger()
  27. def query_yes_no(question, default="yes"):
  28. """Ask a yes/no question via input() and return their answer.
  29. "question" is a string that is presented to the user.
  30. "default" is the presumed answer if the user just hits <Enter>.
  31. It must be "yes" (the default), "no" or None (meaning
  32. an answer is required of the user).
  33. The "answer" return value is True for "yes" or False for "no".
  34. """
  35. valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False}
  36. if default is None:
  37. prompt = " [y/n] "
  38. elif default == "yes":
  39. prompt = " [Y/n] "
  40. elif default == "no":
  41. prompt = " [y/N] "
  42. else:
  43. raise ValueError("invalid default answer: '%s'" % default)
  44. while True:
  45. sys.stdout.write(question + prompt)
  46. choice = input().lower()
  47. if default is not None and choice == "":
  48. return valid[default]
  49. elif choice in valid:
  50. return valid[choice]
  51. else:
  52. sys.stdout.write("Please respond with 'yes' or 'no' " "(or 'y' or 'n').\n")
  53. def gather_profiles(file='~/.aws/config', commercial=False):
  54. file=os.path.expanduser(file)
  55. profiles=list()
  56. try:
  57. with open(os.path.expanduser(file), "r") as configfile:
  58. for line in configfile:
  59. match = re.search('^\[profile (.*)\]', line)
  60. if match:
  61. if 'test' in match.group(1):
  62. if 'gov' in match.group(1) or commercial:
  63. profiles.append(match.group(1))
  64. except Exception as e:
  65. logger.error(f'Could not open file {file} for reading: {e}')
  66. sys.exit(1)
  67. return profiles
  68. def gather_instances(ec2, tagname, tagvalue, exclude_state):
  69. '''
  70. Gathers the instance information from the provided ec2 resource
  71. '''
  72. desired_instances = ec2.instances.filter(
  73. Filters=[
  74. {
  75. 'Name': f'tag:{tagname}',
  76. 'Values': [ f'{tagvalue}' ]
  77. }
  78. ]
  79. )
  80. for i in desired_instances:
  81. name=""
  82. for t in i.tags:
  83. if t['Key']=='Name':
  84. name = t['Value']
  85. logger.debug(f'DEBUG: Discovered instance {i.instance_id}, Name: {name}, State: {i.state["Name"]}')
  86. if i.state['Name'] != exclude_state:
  87. # Why an iterator? Why not?
  88. yield {
  89. 'instance_id': i.instance_id,
  90. 'name': name,
  91. 'state': i.state['Name']
  92. }
  93. if __name__ == "__main__":
  94. handler = logging.StreamHandler()
  95. logger.addHandler(handler)
  96. # Turn off debugging for AWS
  97. for module in [ 'boto3', 'botocore', 'nose', 's3transfer', 'urllib3', 'urllib3.connectionpool' ]:
  98. l = logging.getLogger(module)
  99. l.setLevel(logging.WARNING) # bump this up if you think you have a boto3 bug
  100. parser = argparse.ArgumentParser(description='Startup/Shutdown Groups of Instances')
  101. parser.add_argument('action', help='Action to Take on Instances', choices=['stop', 'start', 'status'])
  102. parser.add_argument('--config', help='AWS Config File', default='~/.aws/config')
  103. parser.add_argument('--profile', help='Profile to Use', default=None)
  104. parser.add_argument('--tagname', help='Tag Name to Filter On', default='Schedule')
  105. parser.add_argument('--tagvalue', help='Tag Value to Filter On', default='MSOC')
  106. parser.add_argument('instancename', help='Instance Name or ID to action on', nargs='?', default=None)
  107. parser.add_argument('--commercial', help='Include Commercial Accounts', action='store_true')
  108. parser.add_argument('--dry-run', help='Dry Run', action='store_true')
  109. parser.add_argument('--debug', help='Output Debug Information', action='store_true')
  110. args = parser.parse_args()
  111. if args.debug:
  112. DEBUG=True
  113. logger.setLevel(logging.DEBUG) # Debug
  114. else:
  115. logger.setLevel(logging.INFO) # Info
  116. if args.profile is not None:
  117. profiles=[ args.profile ]
  118. else:
  119. profiles=gather_profiles(file=args.config, commercial=args.commercial)
  120. logger.debug(f'Profiles to gather: {json.dumps(profiles)}')
  121. # Connect to our profiles:
  122. session = dict()
  123. ec2 = dict()
  124. instances = dict()
  125. instance_count = 0
  126. print(f'Instances to { args.action }:')
  127. for p in profiles:
  128. logger.debug(f'Gathering from profile {p}...')
  129. try:
  130. session[p] = boto3.session.Session(profile_name=p)
  131. ec2[p] = session[p].resource('ec2')
  132. except Exception as e:
  133. logger.error(f'Could not connect to profile "{p}": {e}')
  134. exclude_state = ""
  135. if args.action=="start":
  136. exclude_state="running"
  137. elif args.action=="stop":
  138. exclude_state="stopped"
  139. instances[p] = list()
  140. for instance in gather_instances(ec2[p], args.tagname, args.tagvalue, exclude_state):
  141. if args.instancename is None or args.instancename == instance['name'] or args.instancename == instance['instance_id']:
  142. print(f'{p:<30}\t{ instance["instance_id"] }\t{instance["state"]:<10}\t{instance["name"]}')
  143. instances[p].append(instance['instance_id'])
  144. instance_count += 1
  145. if instance_count == 0:
  146. print('')
  147. print('No applicable instances discovered.')
  148. sys.exit(0)
  149. logger.debug(f'Num instances: {len(instances)}: {json.dumps(instances)}')
  150. if args.action=="status":
  151. logger.debug('Exiting after status check')
  152. sys.exit(0)
  153. print('')
  154. if query_yes_no(question='Continue?', default='no') is False:
  155. logger.debug('Exiting on user request')
  156. sys.exit(2)
  157. for p in profiles:
  158. if len(instances[p]) > 0:
  159. for i in ec2[p].instances.filter(InstanceIds=instances[p]):
  160. if args.action == 'start':
  161. print(f'Starting instances in profile { p }: { instances[p] }')
  162. try:
  163. i.start(DryRun=args.dry_run)
  164. except Exception as e:
  165. print(f'An error occured while starting instance {i.id}. Error: {e}. Skipping.')
  166. elif args.action == 'stop':
  167. print(f'Stopping instances in profile { p }: { instances[p] }')
  168. try:
  169. i.stop(DryRun=args.dry_run)
  170. except Exception as e:
  171. print(f'An error occured while stopping instance {i.id}. Error: {e}. Skipping.')
  172. else:
  173. print(f'Unsupported action: {args.action}')
  174. sys.exit(3)