/plugins/inventory/apache-libcloud.py

https://github.com/ajanthanm/ansible · Python · 355 lines · 178 code · 77 blank · 100 comment · 50 complexity · c322f105d52867b625ddfedb44366b1a MD5 · raw file

  1. #!/usr/bin/env python
  2. # (c) 2013, Sebastien Goasguen <runseb@gmail.com>
  3. #
  4. # This file is part of Ansible,
  5. #
  6. # Ansible is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # Ansible is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
  18. ######################################################################
  19. '''
  20. Apache Libcloud generic external inventory script
  21. =================================
  22. Generates inventory that Ansible can understand by making API request to
  23. Cloud providers using the Apache libcloud library.
  24. This script also assumes there is a libcloud.ini file alongside it
  25. '''
  26. import sys
  27. import os
  28. import argparse
  29. import re
  30. from time import time
  31. import ConfigParser
  32. from libcloud.compute.types import Provider
  33. from libcloud.compute.providers import get_driver
  34. import libcloud.security as sec
  35. try:
  36. import json
  37. except ImportError:
  38. import simplejson as json
  39. class LibcloudInventory(object):
  40. def __init__(self):
  41. ''' Main execution path '''
  42. # Inventory grouped by instance IDs, tags, security groups, regions,
  43. # and availability zones
  44. self.inventory = {}
  45. # Index of hostname (address) to instance ID
  46. self.index = {}
  47. # Read settings and parse CLI arguments
  48. self.read_settings()
  49. self.parse_cli_args()
  50. # Cache
  51. if self.args.refresh_cache:
  52. self.do_api_calls_update_cache()
  53. elif not self.is_cache_valid():
  54. self.do_api_calls_update_cache()
  55. # Data to print
  56. if self.args.host:
  57. data_to_print = self.get_host_info()
  58. elif self.args.list:
  59. # Display list of instances for inventory
  60. if len(self.inventory) == 0:
  61. data_to_print = self.get_inventory_from_cache()
  62. else:
  63. data_to_print = self.json_format_dict(self.inventory, True)
  64. print data_to_print
  65. def is_cache_valid(self):
  66. ''' Determines if the cache files have expired, or if it is still valid '''
  67. if os.path.isfile(self.cache_path_cache):
  68. mod_time = os.path.getmtime(self.cache_path_cache)
  69. current_time = time()
  70. if (mod_time + self.cache_max_age) > current_time:
  71. if os.path.isfile(self.cache_path_index):
  72. return True
  73. return False
  74. def read_settings(self):
  75. ''' Reads the settings from the libcloud.ini file '''
  76. config = ConfigParser.SafeConfigParser()
  77. libcloud_default_ini_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'libcloud.ini')
  78. libcloud_ini_path = os.environ.get('LIBCLOUD_INI_PATH', libcloud_default_ini_path)
  79. config.read(libcloud_ini_path)
  80. if not config.has_section('driver'):
  81. raise ValueError('libcloud.ini file must contain a [driver] section')
  82. if config.has_option('driver', 'provider'):
  83. self.provider = config.get('driver','provider')
  84. else:
  85. raise ValueError('libcloud.ini does not have a provider defined')
  86. if config.has_option('driver', 'key'):
  87. self.key = config.get('driver','key')
  88. else:
  89. raise ValueError('libcloud.ini does not have a key defined')
  90. if config.has_option('driver', 'secret'):
  91. self.secret = config.get('driver','secret')
  92. else:
  93. raise ValueError('libcloud.ini does not have a secret defined')
  94. if config.has_option('driver', 'host'):
  95. self.host = config.get('driver', 'host')
  96. if config.has_option('driver', 'secure'):
  97. self.secure = config.get('driver', 'secure')
  98. if config.has_option('driver', 'verify_ssl_cert'):
  99. self.verify_ssl_cert = config.get('driver', 'verify_ssl_cert')
  100. if config.has_option('driver', 'port'):
  101. self.port = config.get('driver', 'port')
  102. if config.has_option('driver', 'path'):
  103. self.path = config.get('driver', 'path')
  104. if config.has_option('driver', 'api_version'):
  105. self.api_version = config.get('driver', 'api_version')
  106. Driver = get_driver(getattr(Provider, self.provider))
  107. self.conn = Driver(key=self.key, secret=self.secret, secure=self.secure,
  108. host=self.host, path=self.path)
  109. # Cache related
  110. cache_path = config.get('cache', 'cache_path')
  111. self.cache_path_cache = cache_path + "/ansible-libcloud.cache"
  112. self.cache_path_index = cache_path + "/ansible-libcloud.index"
  113. self.cache_max_age = config.getint('cache', 'cache_max_age')
  114. def parse_cli_args(self):
  115. '''
  116. Command line argument processing
  117. '''
  118. parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on libcloud supported providers')
  119. parser.add_argument('--list', action='store_true', default=True,
  120. help='List instances (default: True)')
  121. parser.add_argument('--host', action='store',
  122. help='Get all the variables about a specific instance')
  123. parser.add_argument('--refresh-cache', action='store_true', default=False,
  124. help='Force refresh of cache by making API requests to libcloud supported providers (default: False - use cache files)')
  125. self.args = parser.parse_args()
  126. def do_api_calls_update_cache(self):
  127. '''
  128. Do API calls to a location, and save data in cache files
  129. '''
  130. self.get_nodes()
  131. self.write_to_cache(self.inventory, self.cache_path_cache)
  132. self.write_to_cache(self.index, self.cache_path_index)
  133. def get_nodes(self):
  134. '''
  135. Gets the list of all nodes
  136. '''
  137. for node in self.conn.list_nodes():
  138. self.add_node(node)
  139. def get_node(self, node_id):
  140. '''
  141. Gets details about a specific node
  142. '''
  143. return [node for node in self.conn.list_nodes() if node.id == node_id][0]
  144. def add_node(self, node):
  145. '''
  146. Adds a node to the inventory and index, as long as it is
  147. addressable
  148. '''
  149. # Only want running instances
  150. if node.state != 0:
  151. return
  152. # Select the best destination address
  153. if not node.public_ips == []:
  154. dest = node.public_ips[0]
  155. if not dest:
  156. # Skip instances we cannot address (e.g. private VPC subnet)
  157. return
  158. # Add to index
  159. self.index[dest] = node.name
  160. # Inventory: Group by instance ID (always a group of 1)
  161. self.inventory[node.name] = [dest]
  162. '''
  163. # Inventory: Group by region
  164. self.push(self.inventory, region, dest)
  165. # Inventory: Group by availability zone
  166. self.push(self.inventory, node.placement, dest)
  167. # Inventory: Group by instance type
  168. self.push(self.inventory, self.to_safe('type_' + node.instance_type), dest)
  169. '''
  170. # Inventory: Group by key pair
  171. if node.extra['keyname']:
  172. self.push(self.inventory, self.to_safe('key_' + node.extra['keyname']), dest)
  173. # Inventory: Group by security group, quick thing to handle single sg
  174. if node.extra['securitygroup']:
  175. self.push(self.inventory, self.to_safe('sg_' + node.extra['securitygroup'][0]), dest)
  176. def get_host_info(self):
  177. '''
  178. Get variables about a specific host
  179. '''
  180. if len(self.index) == 0:
  181. # Need to load index from cache
  182. self.load_index_from_cache()
  183. if not self.args.host in self.index:
  184. # try updating the cache
  185. self.do_api_calls_update_cache()
  186. if not self.args.host in self.index:
  187. # host migh not exist anymore
  188. return self.json_format_dict({}, True)
  189. node_id = self.index[self.args.host]
  190. node = self.get_node(node_id)
  191. instance_vars = {}
  192. for key in vars(instance):
  193. value = getattr(instance, key)
  194. key = self.to_safe('ec2_' + key)
  195. # Handle complex types
  196. if type(value) in [int, bool]:
  197. instance_vars[key] = value
  198. elif type(value) in [str, unicode]:
  199. instance_vars[key] = value.strip()
  200. elif type(value) == type(None):
  201. instance_vars[key] = ''
  202. elif key == 'ec2_region':
  203. instance_vars[key] = value.name
  204. elif key == 'ec2_tags':
  205. for k, v in value.iteritems():
  206. key = self.to_safe('ec2_tag_' + k)
  207. instance_vars[key] = v
  208. elif key == 'ec2_groups':
  209. group_ids = []
  210. group_names = []
  211. for group in value:
  212. group_ids.append(group.id)
  213. group_names.append(group.name)
  214. instance_vars["ec2_security_group_ids"] = ','.join(group_ids)
  215. instance_vars["ec2_security_group_names"] = ','.join(group_names)
  216. else:
  217. pass
  218. # TODO Product codes if someone finds them useful
  219. #print key
  220. #print type(value)
  221. #print value
  222. return self.json_format_dict(instance_vars, True)
  223. def push(self, my_dict, key, element):
  224. '''
  225. Pushed an element onto an array that may not have been defined in
  226. the dict
  227. '''
  228. if key in my_dict:
  229. my_dict[key].append(element);
  230. else:
  231. my_dict[key] = [element]
  232. def get_inventory_from_cache(self):
  233. '''
  234. Reads the inventory from the cache file and returns it as a JSON
  235. object
  236. '''
  237. cache = open(self.cache_path_cache, 'r')
  238. json_inventory = cache.read()
  239. return json_inventory
  240. def load_index_from_cache(self):
  241. '''
  242. Reads the index from the cache file sets self.index
  243. '''
  244. cache = open(self.cache_path_index, 'r')
  245. json_index = cache.read()
  246. self.index = json.loads(json_index)
  247. def write_to_cache(self, data, filename):
  248. '''
  249. Writes data in JSON format to a file
  250. '''
  251. json_data = self.json_format_dict(data, True)
  252. cache = open(filename, 'w')
  253. cache.write(json_data)
  254. cache.close()
  255. def to_safe(self, word):
  256. '''
  257. Converts 'bad' characters in a string to underscores so they can be
  258. used as Ansible groups
  259. '''
  260. return re.sub("[^A-Za-z0-9\-]", "_", word)
  261. def json_format_dict(self, data, pretty=False):
  262. '''
  263. Converts a dict to a JSON object and dumps it as a formatted
  264. string
  265. '''
  266. if pretty:
  267. return json.dumps(data, sort_keys=True, indent=2)
  268. else:
  269. return json.dumps(data)
  270. def main():
  271. LibcloudInventory()
  272. if __name__ == '__main__':
  273. main()