multi_ec2.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. #!/usr/bin/env python
  2. from time import time
  3. import argparse
  4. import yaml
  5. import os
  6. import sys
  7. import pdb
  8. import subprocess
  9. import json
  10. import pprint
  11. class MultiEc2(object):
  12. def __init__(self):
  13. self.config = None
  14. self.results = {}
  15. self.result = {}
  16. self.cache_path_cache = os.path.expanduser('~/.ansible/tmp/multi_ec2_inventory.cache')
  17. self.file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)))
  18. self.parse_cli_args()
  19. # load yaml
  20. self.load_yaml_config()
  21. # if its a host query, fetch and do not cache
  22. if self.args.host:
  23. self.get_inventory()
  24. elif not self.is_cache_valid():
  25. # go fetch the inventories and cache them if cache is expired
  26. self.get_inventory()
  27. self.write_to_cache()
  28. else:
  29. # get data from disk
  30. self.get_inventory_from_cache()
  31. def load_yaml_config(self,conf_file=None):
  32. """Load a yaml config file with credentials to query the
  33. respective cloud for inventory.
  34. """
  35. config = None
  36. if not conf_file:
  37. conf_file = os.path.join(self.file_path,'multi_ec2.yaml')
  38. with open(conf_file) as conf:
  39. self.config = yaml.safe_load(conf)
  40. def get_provider_tags(self,provider, env={}):
  41. """Call <provider> and query all of the tags that are usuable
  42. by ansible. If environment is empty use the default env.
  43. """
  44. if not env:
  45. env = os.environ
  46. # check to see if provider exists
  47. if not os.path.isfile(provider) or not os.access(provider, os.X_OK):
  48. raise RuntimeError("Problem with the provider. Please check path " \
  49. "and that it is executable. (%s)" % provider)
  50. cmds = [provider]
  51. if self.args.host:
  52. cmds.append("--host")
  53. cmds.append(self.args.host)
  54. else:
  55. cmds.append('--list')
  56. cmds.append('--refresh-cache')
  57. return subprocess.Popen(cmds, stderr=subprocess.PIPE, \
  58. stdout=subprocess.PIPE, env=env)
  59. def get_inventory(self):
  60. """Create the subprocess to fetch tags from a provider.
  61. Host query:
  62. Query to return a specific host. If > 1 queries have
  63. results then fail.
  64. List query:
  65. Query all of the different accounts for their tags. Once completed
  66. store all of their results into one merged updated hash.
  67. """
  68. processes = {}
  69. for account in self.config['accounts']:
  70. env = account['env_vars']
  71. name = account['name']
  72. provider = account['provider']
  73. processes[name] = self.get_provider_tags(provider, env)
  74. # for each process collect stdout when its available
  75. all_results = []
  76. for name, process in processes.items():
  77. out, err = process.communicate()
  78. all_results.append({
  79. "name": name,
  80. "out": out.strip(),
  81. "err": err.strip(),
  82. "code": process.returncode
  83. })
  84. if not self.args.host:
  85. # For any non-zero, raise an error on it
  86. for result in all_results:
  87. if result['code'] != 0:
  88. raise RuntimeError(result['err'])
  89. else:
  90. self.results[result['name']] = json.loads(result['out'])
  91. values = self.results.values()
  92. values.insert(0, self.result)
  93. map(lambda x: self.merge_destructively(self.result, x), values)
  94. else:
  95. # For any 0 result, return it
  96. count = 0
  97. for results in all_results:
  98. if results['code'] == 0 and results['err'] == '' and results['out'] != '{}':
  99. self.result = json.loads(out)
  100. count += 1
  101. if count > 1:
  102. raise RuntimeError("Found > 1 results for --host %s. \
  103. This is an invalid state." % self.args.host)
  104. def merge_destructively(self, a, b):
  105. "merges b into a"
  106. for key in b:
  107. if key in a:
  108. if isinstance(a[key], dict) and isinstance(b[key], dict):
  109. self.merge_destructively(a[key], b[key])
  110. elif a[key] == b[key]:
  111. pass # same leaf value
  112. # both lists so add each element in b to a if it does ! exist
  113. elif isinstance(a[key], list) and isinstance(b[key],list):
  114. for x in b[key]:
  115. if x not in a[key]:
  116. a[key].append(x)
  117. # a is a list and not b
  118. elif isinstance(a[key], list):
  119. if b[key] not in a[key]:
  120. a[key].append(b[key])
  121. elif isinstance(b[key], list):
  122. a[key] = [a[key]] + [k for k in b[key] if k != a[key]]
  123. else:
  124. a[key] = [a[key],b[key]]
  125. else:
  126. a[key] = b[key]
  127. return a
  128. def is_cache_valid(self):
  129. ''' Determines if the cache files have expired, or if it is still valid '''
  130. if os.path.isfile(self.cache_path_cache):
  131. mod_time = os.path.getmtime(self.cache_path_cache)
  132. current_time = time()
  133. if (mod_time + self.config['cache_max_age']) > current_time:
  134. #if os.path.isfile(self.cache_path_index):
  135. return True
  136. return False
  137. def parse_cli_args(self):
  138. ''' Command line argument processing '''
  139. parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on a provider')
  140. parser.add_argument('--list', action='store_true', default=True,
  141. help='List instances (default: True)')
  142. parser.add_argument('--host', action='store',
  143. help='Get all the variables about a specific instance')
  144. self.args = parser.parse_args()
  145. def write_to_cache(self):
  146. ''' Writes data in JSON format to a file '''
  147. json_data = self.json_format_dict(self.result, True)
  148. with open(self.cache_path_cache, 'w') as cache:
  149. cache.write(json_data)
  150. def get_inventory_from_cache(self):
  151. ''' Reads the inventory from the cache file and returns it as a JSON
  152. object '''
  153. with open(self.cache_path_cache, 'r') as cache:
  154. self.result = json.loads(cache.read())
  155. def json_format_dict(self, data, pretty=False):
  156. ''' Converts a dict to a JSON object and dumps it as a formatted
  157. string '''
  158. if pretty:
  159. return json.dumps(data, sort_keys=True, indent=2)
  160. else:
  161. return json.dumps(data)
  162. if __name__ == "__main__":
  163. mi = MultiEc2()
  164. #print mi.result
  165. pp = pprint.PrettyPrinter(indent=2)
  166. pp.pprint(mi.result)