hosts.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. #!/usr/bin/env python
  2. # Copyright 2013 Google Inc.
  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. # This is a derivative of gce.py that adds support for filtering
  19. # the returned inventory to only include instances that have tags
  20. # as specified by GCE_TAGGED_INSTANCES. This prevents dynamic
  21. # inventory for multiple clusters within the same project from
  22. # accidentally stomping each other.
  23. # pylint: skip-file
  24. '''
  25. GCE external inventory script
  26. =================================
  27. Generates inventory that Ansible can understand by making API requests
  28. Google Compute Engine via the libcloud library. Full install/configuration
  29. instructions for the gce* modules can be found in the comments of
  30. ansible/test/gce_tests.py.
  31. When run against a specific host, this script returns the following variables
  32. based on the data obtained from the libcloud Node object:
  33. - gce_uuid
  34. - gce_id
  35. - gce_image
  36. - gce_machine_type
  37. - gce_private_ip
  38. - gce_public_ip
  39. - gce_name
  40. - gce_description
  41. - gce_status
  42. - gce_zone
  43. - gce_tags
  44. - gce_metadata
  45. - gce_network
  46. When run in --list mode, instances are grouped by the following categories:
  47. - zone:
  48. zone group name examples are us-central1-b, europe-west1-a, etc.
  49. - instance tags:
  50. An entry is created for each tag. For example, if you have two instances
  51. with a common tag called 'foo', they will both be grouped together under
  52. the 'tag_foo' name.
  53. - network name:
  54. the name of the network is appended to 'network_' (e.g. the 'default'
  55. network will result in a group named 'network_default')
  56. - machine type
  57. types follow a pattern like n1-standard-4, g1-small, etc.
  58. - running status:
  59. group name prefixed with 'status_' (e.g. status_running, status_stopped,..)
  60. - image:
  61. when using an ephemeral/scratch disk, this will be set to the image name
  62. used when creating the instance (e.g. debian-7-wheezy-v20130816). when
  63. your instance was created with a root persistent disk it will be set to
  64. 'persistent_disk' since there is no current way to determine the image.
  65. Examples:
  66. Execute uname on all instances in the us-central1-a zone
  67. $ ansible -i gce.py us-central1-a -m shell -a "/bin/uname -a"
  68. Use the GCE inventory script to print out instance specific information
  69. $ contrib/inventory/gce.py --host my_instance
  70. Author: Eric Johnson <erjohnso@google.com>
  71. Contributors: Matt Hite <mhite@hotmail.com>
  72. Version: 0.0.2
  73. '''
  74. __requires__ = ['pycrypto>=2.6']
  75. try:
  76. import pkg_resources
  77. except ImportError:
  78. # Use pkg_resources to find the correct versions of libraries and set
  79. # sys.path appropriately when there are multiversion installs. We don't
  80. # fail here as there is code that better expresses the errors where the
  81. # library is used.
  82. pass
  83. USER_AGENT_PRODUCT="Ansible-gce_inventory_plugin"
  84. USER_AGENT_VERSION="v2"
  85. import sys
  86. import os
  87. import time
  88. import argparse
  89. import ConfigParser
  90. import logging
  91. logging.getLogger('libcloud.common.google').addHandler(logging.NullHandler())
  92. try:
  93. import json
  94. except ImportError:
  95. import simplejson as json
  96. try:
  97. from libcloud.compute.types import Provider
  98. from libcloud.compute.providers import get_driver
  99. from libcloud.common.google import ResourceNotFoundError
  100. _ = Provider.GCE
  101. except:
  102. sys.exit("GCE inventory script requires libcloud >= 0.13")
  103. class GceInventory(object):
  104. def __init__(self):
  105. # Read settings and parse CLI arguments
  106. self.parse_cli_args()
  107. self.config = self.get_config()
  108. self.driver = self.get_gce_driver()
  109. self.ip_type = self.get_inventory_options()
  110. if self.ip_type:
  111. self.ip_type = self.ip_type.lower()
  112. # Just display data for specific host
  113. if self.args.host:
  114. print(self.json_format_dict(self.node_to_dict(
  115. self.get_instance(self.args.host)),
  116. pretty=self.args.pretty))
  117. sys.exit(0)
  118. zones = self.parse_env_zones()
  119. # Otherwise, assume user wants all instances grouped
  120. print(self.json_format_dict(self.group_instances(zones),
  121. pretty=self.args.pretty))
  122. sys.exit(0)
  123. def get_config(self):
  124. """
  125. Populates a SafeConfigParser object with defaults and
  126. attempts to read an .ini-style configuration from the filename
  127. specified in GCE_INI_PATH. If the environment variable is
  128. not present, the filename defaults to gce.ini in the current
  129. working directory.
  130. """
  131. gce_ini_default_path = os.path.join(
  132. os.path.dirname(os.path.realpath(__file__)), "gce.ini")
  133. gce_ini_path = os.environ.get('GCE_INI_PATH', gce_ini_default_path)
  134. # Create a ConfigParser.
  135. # This provides empty defaults to each key, so that environment
  136. # variable configuration (as opposed to INI configuration) is able
  137. # to work.
  138. config = ConfigParser.SafeConfigParser(defaults={
  139. 'gce_service_account_email_address': '',
  140. 'gce_service_account_pem_file_path': '',
  141. 'gce_project_id': '',
  142. 'libcloud_secrets': '',
  143. 'inventory_ip_type': '',
  144. })
  145. if 'gce' not in config.sections():
  146. config.add_section('gce')
  147. if 'inventory' not in config.sections():
  148. config.add_section('inventory')
  149. config.read(gce_ini_path)
  150. #########
  151. # Section added for processing ini settings
  152. #########
  153. # Set the instance_states filter based on config file options
  154. self.instance_states = []
  155. if config.has_option('gce', 'instance_states'):
  156. states = config.get('gce', 'instance_states')
  157. # Ignore if instance_states is an empty string.
  158. if states:
  159. self.instance_states = states.split(',')
  160. return config
  161. def get_inventory_options(self):
  162. """Determine inventory options. Environment variables always
  163. take precedence over configuration files."""
  164. ip_type = self.config.get('inventory', 'inventory_ip_type')
  165. # If the appropriate environment variables are set, they override
  166. # other configuration
  167. ip_type = os.environ.get('INVENTORY_IP_TYPE', ip_type)
  168. return ip_type
  169. def get_gce_driver(self):
  170. """Determine the GCE authorization settings and return a
  171. libcloud driver.
  172. """
  173. # Attempt to get GCE params from a configuration file, if one
  174. # exists.
  175. secrets_path = self.config.get('gce', 'libcloud_secrets')
  176. secrets_found = False
  177. try:
  178. import secrets
  179. args = list(getattr(secrets, 'GCE_PARAMS', []))
  180. kwargs = getattr(secrets, 'GCE_KEYWORD_PARAMS', {})
  181. secrets_found = True
  182. except:
  183. pass
  184. if not secrets_found and secrets_path:
  185. if not secrets_path.endswith('secrets.py'):
  186. err = "Must specify libcloud secrets file as "
  187. err += "/absolute/path/to/secrets.py"
  188. sys.exit(err)
  189. sys.path.append(os.path.dirname(secrets_path))
  190. try:
  191. import secrets
  192. args = list(getattr(secrets, 'GCE_PARAMS', []))
  193. kwargs = getattr(secrets, 'GCE_KEYWORD_PARAMS', {})
  194. secrets_found = True
  195. except:
  196. pass
  197. if not secrets_found:
  198. args = [
  199. self.config.get('gce','gce_service_account_email_address'),
  200. self.config.get('gce','gce_service_account_pem_file_path')
  201. ]
  202. kwargs = {'project': self.config.get('gce', 'gce_project_id')}
  203. # If the appropriate environment variables are set, they override
  204. # other configuration; process those into our args and kwargs.
  205. args[0] = os.environ.get('GCE_EMAIL', args[0])
  206. args[1] = os.environ.get('GCE_PEM_FILE_PATH', args[1])
  207. kwargs['project'] = os.environ.get('GCE_PROJECT', kwargs['project'])
  208. # Retrieve and return the GCE driver.
  209. gce = get_driver(Provider.GCE)(*args, **kwargs)
  210. gce.connection.user_agent_append(
  211. '%s/%s' % (USER_AGENT_PRODUCT, USER_AGENT_VERSION),
  212. )
  213. return gce
  214. def parse_env_zones(self):
  215. '''returns a list of comma seperated zones parsed from the GCE_ZONE environment variable.
  216. If provided, this will be used to filter the results of the grouped_instances call'''
  217. import csv
  218. reader = csv.reader([os.environ.get('GCE_ZONE',"")], skipinitialspace=True)
  219. zones = [r for r in reader]
  220. return [z for z in zones[0]]
  221. def parse_cli_args(self):
  222. ''' Command line argument processing '''
  223. parser = argparse.ArgumentParser(
  224. description='Produce an Ansible Inventory file based on GCE')
  225. parser.add_argument('--list', action='store_true', default=True,
  226. help='List instances (default: True)')
  227. parser.add_argument('--host', action='store',
  228. help='Get all information about an instance')
  229. parser.add_argument('--tagged', action='store',
  230. help='Only include instances with this tag')
  231. parser.add_argument('--pretty', action='store_true', default=False,
  232. help='Pretty format (default: False)')
  233. self.args = parser.parse_args()
  234. tag_env = os.environ.get('GCE_TAGGED_INSTANCES')
  235. if not self.args.tagged and tag_env:
  236. self.args.tagged = tag_env
  237. def node_to_dict(self, inst):
  238. md = {}
  239. if inst is None:
  240. return {}
  241. if inst.extra['metadata'].has_key('items'):
  242. for entry in inst.extra['metadata']['items']:
  243. md[entry['key']] = entry['value']
  244. net = inst.extra['networkInterfaces'][0]['network'].split('/')[-1]
  245. # default to exernal IP unless user has specified they prefer internal
  246. if self.ip_type == 'internal':
  247. ssh_host = inst.private_ips[0]
  248. else:
  249. ssh_host = inst.public_ips[0] if len(inst.public_ips) >= 1 else inst.private_ips[0]
  250. return {
  251. 'gce_uuid': inst.uuid,
  252. 'gce_id': inst.id,
  253. 'gce_image': inst.image,
  254. 'gce_machine_type': inst.size,
  255. 'gce_private_ip': inst.private_ips[0],
  256. 'gce_public_ip': inst.public_ips[0] if len(inst.public_ips) >= 1 else None,
  257. 'gce_name': inst.name,
  258. 'gce_description': inst.extra['description'],
  259. 'gce_status': inst.extra['status'],
  260. 'gce_zone': inst.extra['zone'].name,
  261. 'gce_tags': inst.extra['tags'],
  262. 'gce_metadata': md,
  263. 'gce_network': net,
  264. # Hosts don't have a public name, so we add an IP
  265. 'ansible_host': ssh_host
  266. }
  267. def get_instance(self, instance_name):
  268. '''Gets details about a specific instance '''
  269. try:
  270. return self.driver.ex_get_node(instance_name)
  271. except Exception as e:
  272. return None
  273. def group_instances(self, zones=None):
  274. '''Group all instances'''
  275. groups = {}
  276. meta = {}
  277. meta["hostvars"] = {}
  278. # list_nodes will fail if a disk is in the process of being deleted
  279. # from a node, which is not uncommon if other playbooks are managing
  280. # the same project. Retry if we receive a not found error.
  281. nodes = []
  282. tries = 0
  283. while True:
  284. try:
  285. nodes = self.driver.list_nodes()
  286. break
  287. except ResourceNotFoundError:
  288. tries = tries + 1
  289. if tries > 15:
  290. raise e
  291. time.sleep(1)
  292. continue
  293. for node in nodes:
  294. # This check filters on the desired instance states defined in the
  295. # config file with the instance_states config option.
  296. #
  297. # If the instance_states list is _empty_ then _ALL_ states are returned.
  298. #
  299. # If the instance_states list is _populated_ then check the current
  300. # state against the instance_states list
  301. if self.instance_states and not node.extra['status'] in self.instance_states:
  302. continue
  303. name = node.name
  304. if self.args.tagged and self.args.tagged not in node.extra['tags']:
  305. continue
  306. meta["hostvars"][name] = self.node_to_dict(node)
  307. zone = node.extra['zone'].name
  308. # To avoid making multiple requests per zone
  309. # we list all nodes and then filter the results
  310. if zones and zone not in zones:
  311. continue
  312. if groups.has_key(zone): groups[zone].append(name)
  313. else: groups[zone] = [name]
  314. tags = node.extra['tags']
  315. for t in tags:
  316. if t.startswith('group-'):
  317. tag = t[6:]
  318. else:
  319. tag = 'tag_%s' % t
  320. if groups.has_key(tag): groups[tag].append(name)
  321. else: groups[tag] = [name]
  322. net = node.extra['networkInterfaces'][0]['network'].split('/')[-1]
  323. net = 'network_%s' % net
  324. if groups.has_key(net): groups[net].append(name)
  325. else: groups[net] = [name]
  326. machine_type = node.size
  327. if groups.has_key(machine_type): groups[machine_type].append(name)
  328. else: groups[machine_type] = [name]
  329. image = node.image and node.image or 'persistent_disk'
  330. if groups.has_key(image): groups[image].append(name)
  331. else: groups[image] = [name]
  332. status = node.extra['status']
  333. stat = 'status_%s' % status.lower()
  334. if groups.has_key(stat): groups[stat].append(name)
  335. else: groups[stat] = [name]
  336. groups["_meta"] = meta
  337. return groups
  338. def json_format_dict(self, data, pretty=False):
  339. ''' Converts a dict to a JSON object and dumps it as a formatted
  340. string '''
  341. if pretty:
  342. return json.dumps(data, sort_keys=True, indent=2)
  343. else:
  344. return json.dumps(data)
  345. # Run the script
  346. GceInventory()