xdrtest 7.8 KB

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