ec2.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. #!/usr/bin/env python
  2. '''
  3. EC2 external inventory script
  4. =================================
  5. Generates inventory that Ansible can understand by making API request to
  6. AWS EC2 using the Boto library.
  7. NOTE: This script assumes Ansible is being executed where the environment
  8. variables needed for Boto have already been set:
  9. export AWS_ACCESS_KEY_ID='AK123'
  10. export AWS_SECRET_ACCESS_KEY='abc123'
  11. This script also assumes there is an ec2.ini file alongside it. To specify a
  12. different path to ec2.ini, define the EC2_INI_PATH environment variable:
  13. export EC2_INI_PATH=/path/to/my_ec2.ini
  14. If you're using eucalyptus you need to set the above variables and
  15. you need to define:
  16. export EC2_URL=http://hostname_of_your_cc:port/services/Eucalyptus
  17. For more details, see: http://docs.pythonboto.org/en/latest/boto_config_tut.html
  18. When run against a specific host, this script returns the following variables:
  19. - ec2_ami_launch_index
  20. - ec2_architecture
  21. - ec2_association
  22. - ec2_attachTime
  23. - ec2_attachment
  24. - ec2_attachmentId
  25. - ec2_client_token
  26. - ec2_deleteOnTermination
  27. - ec2_description
  28. - ec2_deviceIndex
  29. - ec2_dns_name
  30. - ec2_eventsSet
  31. - ec2_group_name
  32. - ec2_hypervisor
  33. - ec2_id
  34. - ec2_image_id
  35. - ec2_instanceState
  36. - ec2_instance_type
  37. - ec2_ipOwnerId
  38. - ec2_ip_address
  39. - ec2_item
  40. - ec2_kernel
  41. - ec2_key_name
  42. - ec2_launch_time
  43. - ec2_monitored
  44. - ec2_monitoring
  45. - ec2_networkInterfaceId
  46. - ec2_ownerId
  47. - ec2_persistent
  48. - ec2_placement
  49. - ec2_platform
  50. - ec2_previous_state
  51. - ec2_private_dns_name
  52. - ec2_private_ip_address
  53. - ec2_publicIp
  54. - ec2_public_dns_name
  55. - ec2_ramdisk
  56. - ec2_reason
  57. - ec2_region
  58. - ec2_requester_id
  59. - ec2_root_device_name
  60. - ec2_root_device_type
  61. - ec2_security_group_ids
  62. - ec2_security_group_names
  63. - ec2_shutdown_state
  64. - ec2_sourceDestCheck
  65. - ec2_spot_instance_request_id
  66. - ec2_state
  67. - ec2_state_code
  68. - ec2_state_reason
  69. - ec2_status
  70. - ec2_subnet_id
  71. - ec2_tenancy
  72. - ec2_virtualization_type
  73. - ec2_vpc_id
  74. These variables are pulled out of a boto.ec2.instance object. There is a lack of
  75. consistency with variable spellings (camelCase and underscores) since this
  76. just loops through all variables the object exposes. It is preferred to use the
  77. ones with underscores when multiple exist.
  78. In addition, if an instance has AWS Tags associated with it, each tag is a new
  79. variable named:
  80. - ec2_tag_[Key] = [Value]
  81. Security groups are comma-separated in 'ec2_security_group_ids' and
  82. 'ec2_security_group_names'.
  83. '''
  84. # (c) 2012, Peter Sankauskas
  85. #
  86. # This file is part of Ansible,
  87. #
  88. # Ansible is free software: you can redistribute it and/or modify
  89. # it under the terms of the GNU General Public License as published by
  90. # the Free Software Foundation, either version 3 of the License, or
  91. # (at your option) any later version.
  92. #
  93. # Ansible is distributed in the hope that it will be useful,
  94. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  95. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  96. # GNU General Public License for more details.
  97. #
  98. # You should have received a copy of the GNU General Public License
  99. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
  100. ######################################################################
  101. import sys
  102. import os
  103. import argparse
  104. import re
  105. from time import time
  106. import boto
  107. from boto import ec2
  108. from boto import rds
  109. from boto import route53
  110. import ConfigParser
  111. from collections import defaultdict
  112. try:
  113. import json
  114. except ImportError:
  115. import simplejson as json
  116. class Ec2Inventory(object):
  117. def _empty_inventory(self):
  118. return {"_meta" : {"hostvars" : {}}}
  119. def __init__(self):
  120. ''' Main execution path '''
  121. # Inventory grouped by instance IDs, tags, security groups, regions,
  122. # and availability zones
  123. self.inventory = self._empty_inventory()
  124. # Index of hostname (address) to instance ID
  125. self.index = {}
  126. # Read settings and parse CLI arguments
  127. self.read_settings()
  128. self.parse_cli_args()
  129. # Cache
  130. if self.args.refresh_cache:
  131. self.do_api_calls_update_cache()
  132. elif not self.is_cache_valid():
  133. self.do_api_calls_update_cache()
  134. # Data to print
  135. if self.args.host:
  136. data_to_print = self.get_host_info()
  137. elif self.args.list:
  138. # Display list of instances for inventory
  139. if self.inventory == self._empty_inventory():
  140. data_to_print = self.get_inventory_from_cache()
  141. else:
  142. data_to_print = self.json_format_dict(self.inventory, True)
  143. print data_to_print
  144. def is_cache_valid(self):
  145. ''' Determines if the cache files have expired, or if it is still valid '''
  146. if os.path.isfile(self.cache_path_cache):
  147. mod_time = os.path.getmtime(self.cache_path_cache)
  148. current_time = time()
  149. if (mod_time + self.cache_max_age) > current_time:
  150. if os.path.isfile(self.cache_path_index):
  151. return True
  152. return False
  153. def read_settings(self):
  154. ''' Reads the settings from the ec2.ini file '''
  155. config = ConfigParser.SafeConfigParser()
  156. ec2_default_ini_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ec2.ini')
  157. ec2_ini_path = os.environ.get('EC2_INI_PATH', ec2_default_ini_path)
  158. config.read(ec2_ini_path)
  159. # is eucalyptus?
  160. self.eucalyptus_host = None
  161. self.eucalyptus = False
  162. if config.has_option('ec2', 'eucalyptus'):
  163. self.eucalyptus = config.getboolean('ec2', 'eucalyptus')
  164. if self.eucalyptus and config.has_option('ec2', 'eucalyptus_host'):
  165. self.eucalyptus_host = config.get('ec2', 'eucalyptus_host')
  166. # Regions
  167. self.regions = []
  168. configRegions = config.get('ec2', 'regions')
  169. configRegions_exclude = config.get('ec2', 'regions_exclude')
  170. if (configRegions == 'all'):
  171. if self.eucalyptus_host:
  172. self.regions.append(boto.connect_euca(host=self.eucalyptus_host).region.name)
  173. else:
  174. for regionInfo in ec2.regions():
  175. if regionInfo.name not in configRegions_exclude:
  176. self.regions.append(regionInfo.name)
  177. else:
  178. self.regions = configRegions.split(",")
  179. # Destination addresses
  180. self.destination_variable = config.get('ec2', 'destination_variable')
  181. self.vpc_destination_variable = config.get('ec2', 'vpc_destination_variable')
  182. self.destination_format = config.get('ec2', 'destination_format')
  183. self.destination_format_tags = config.get('ec2', 'destination_format_tags', '').split(',')
  184. # Route53
  185. self.route53_enabled = config.getboolean('ec2', 'route53')
  186. self.route53_excluded_zones = []
  187. if config.has_option('ec2', 'route53_excluded_zones'):
  188. self.route53_excluded_zones.extend(
  189. config.get('ec2', 'route53_excluded_zones', '').split(','))
  190. # Include RDS instances?
  191. self.rds_enabled = True
  192. if config.has_option('ec2', 'rds'):
  193. self.rds_enabled = config.getboolean('ec2', 'rds')
  194. # Return all EC2 and RDS instances (if RDS is enabled)
  195. if config.has_option('ec2', 'all_instances'):
  196. self.all_instances = config.getboolean('ec2', 'all_instances')
  197. else:
  198. self.all_instances = False
  199. if config.has_option('ec2', 'all_rds_instances') and self.rds_enabled:
  200. self.all_rds_instances = config.getboolean('ec2', 'all_rds_instances')
  201. else:
  202. self.all_rds_instances = False
  203. # Cache related
  204. cache_dir = os.path.expanduser(config.get('ec2', 'cache_path'))
  205. if not os.path.exists(cache_dir):
  206. os.makedirs(cache_dir)
  207. self.cache_path_cache = cache_dir + "/ansible-ec2.cache"
  208. self.cache_path_index = cache_dir + "/ansible-ec2.index"
  209. self.cache_max_age = config.getint('ec2', 'cache_max_age')
  210. # Configure nested groups instead of flat namespace.
  211. if config.has_option('ec2', 'nested_groups'):
  212. self.nested_groups = config.getboolean('ec2', 'nested_groups')
  213. else:
  214. self.nested_groups = False
  215. # Configure which groups should be created.
  216. group_by_options = [
  217. 'group_by_instance_id',
  218. 'group_by_region',
  219. 'group_by_availability_zone',
  220. 'group_by_ami_id',
  221. 'group_by_instance_type',
  222. 'group_by_key_pair',
  223. 'group_by_vpc_id',
  224. 'group_by_security_group',
  225. 'group_by_tag_keys',
  226. 'group_by_tag_none',
  227. 'group_by_route53_names',
  228. 'group_by_rds_engine',
  229. 'group_by_rds_parameter_group',
  230. ]
  231. for option in group_by_options:
  232. if config.has_option('ec2', option):
  233. setattr(self, option, config.getboolean('ec2', option))
  234. else:
  235. setattr(self, option, True)
  236. # Do we need to just include hosts that match a pattern?
  237. try:
  238. pattern_include = config.get('ec2', 'pattern_include')
  239. if pattern_include and len(pattern_include) > 0:
  240. self.pattern_include = re.compile(pattern_include)
  241. else:
  242. self.pattern_include = None
  243. except ConfigParser.NoOptionError, e:
  244. self.pattern_include = None
  245. # Do we need to exclude hosts that match a pattern?
  246. try:
  247. pattern_exclude = config.get('ec2', 'pattern_exclude');
  248. if pattern_exclude and len(pattern_exclude) > 0:
  249. self.pattern_exclude = re.compile(pattern_exclude)
  250. else:
  251. self.pattern_exclude = None
  252. except ConfigParser.NoOptionError, e:
  253. self.pattern_exclude = None
  254. # Instance filters (see boto and EC2 API docs). Ignore invalid filters.
  255. self.ec2_instance_filters = defaultdict(list)
  256. if config.has_option('ec2', 'instance_filters'):
  257. for instance_filter in config.get('ec2', 'instance_filters', '').split(','):
  258. instance_filter = instance_filter.strip()
  259. if not instance_filter or '=' not in instance_filter:
  260. continue
  261. filter_key, filter_value = [x.strip() for x in instance_filter.split('=', 1)]
  262. if not filter_key:
  263. continue
  264. self.ec2_instance_filters[filter_key].append(filter_value)
  265. def parse_cli_args(self):
  266. ''' Command line argument processing '''
  267. parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on EC2')
  268. parser.add_argument('--list', action='store_true', default=True,
  269. help='List instances (default: True)')
  270. parser.add_argument('--host', action='store',
  271. help='Get all the variables about a specific instance')
  272. parser.add_argument('--refresh-cache', action='store_true', default=False,
  273. help='Force refresh of cache by making API requests to EC2 (default: False - use cache files)')
  274. self.args = parser.parse_args()
  275. def do_api_calls_update_cache(self):
  276. ''' Do API calls to each region, and save data in cache files '''
  277. if self.route53_enabled:
  278. self.get_route53_records()
  279. for region in self.regions:
  280. self.get_instances_by_region(region)
  281. if self.rds_enabled:
  282. self.get_rds_instances_by_region(region)
  283. self.write_to_cache(self.inventory, self.cache_path_cache)
  284. self.write_to_cache(self.index, self.cache_path_index)
  285. def get_instances_by_region(self, region):
  286. ''' Makes an AWS EC2 API call to the list of instances in a particular
  287. region '''
  288. try:
  289. if self.eucalyptus:
  290. conn = boto.connect_euca(host=self.eucalyptus_host)
  291. conn.APIVersion = '2010-08-31'
  292. else:
  293. conn = ec2.connect_to_region(region)
  294. # connect_to_region will fail "silently" by returning None if the region name is wrong or not supported
  295. if conn is None:
  296. print("region name: %s likely not supported, or AWS is down. connection to region failed." % region)
  297. sys.exit(1)
  298. reservations = []
  299. if self.ec2_instance_filters:
  300. for filter_key, filter_values in self.ec2_instance_filters.iteritems():
  301. reservations.extend(conn.get_all_instances(filters = { filter_key : filter_values }))
  302. else:
  303. reservations = conn.get_all_instances()
  304. for reservation in reservations:
  305. for instance in reservation.instances:
  306. self.add_instance(instance, region)
  307. except boto.exception.BotoServerError, e:
  308. if not self.eucalyptus:
  309. print "Looks like AWS is down again:"
  310. print e
  311. sys.exit(1)
  312. def get_rds_instances_by_region(self, region):
  313. ''' Makes an AWS API call to the list of RDS instances in a particular
  314. region '''
  315. try:
  316. conn = rds.connect_to_region(region)
  317. if conn:
  318. instances = conn.get_all_dbinstances()
  319. for instance in instances:
  320. self.add_rds_instance(instance, region)
  321. except boto.exception.BotoServerError, e:
  322. if not e.reason == "Forbidden":
  323. print "Looks like AWS RDS is down: "
  324. print e
  325. sys.exit(1)
  326. def get_instance(self, region, instance_id):
  327. ''' Gets details about a specific instance '''
  328. if self.eucalyptus:
  329. conn = boto.connect_euca(self.eucalyptus_host)
  330. conn.APIVersion = '2010-08-31'
  331. else:
  332. conn = ec2.connect_to_region(region)
  333. # connect_to_region will fail "silently" by returning None if the region name is wrong or not supported
  334. if conn is None:
  335. print("region name: %s likely not supported, or AWS is down. connection to region failed." % region)
  336. sys.exit(1)
  337. reservations = conn.get_all_instances([instance_id])
  338. for reservation in reservations:
  339. for instance in reservation.instances:
  340. return instance
  341. def add_instance(self, instance, region):
  342. ''' Adds an instance to the inventory and index, as long as it is
  343. addressable '''
  344. # Only want running instances unless all_instances is True
  345. if not self.all_instances and instance.state != 'running':
  346. return
  347. # Select the best destination address
  348. if self.destination_format and self.destination_format_tags:
  349. dest = self.destination_format.format(*[ getattr(instance, 'tags').get(tag, 'nil') for tag in self.destination_format_tags ])
  350. elif instance.subnet_id:
  351. dest = getattr(instance, self.vpc_destination_variable, None)
  352. if dest is None:
  353. dest = getattr(instance, 'tags').get(self.vpc_destination_variable, None)
  354. else:
  355. dest = getattr(instance, self.destination_variable, None)
  356. if dest is None:
  357. dest = getattr(instance, 'tags').get(self.destination_variable, None)
  358. if not dest:
  359. # Skip instances we cannot address (e.g. private VPC subnet)
  360. return
  361. # if we only want to include hosts that match a pattern, skip those that don't
  362. if self.pattern_include and not self.pattern_include.match(dest):
  363. return
  364. # if we need to exclude hosts that match a pattern, skip those
  365. if self.pattern_exclude and self.pattern_exclude.match(dest):
  366. return
  367. # Add to index
  368. self.index[dest] = [region, instance.id]
  369. # Inventory: Group by instance ID (always a group of 1)
  370. if self.group_by_instance_id:
  371. self.inventory[instance.id] = [dest]
  372. if self.nested_groups:
  373. self.push_group(self.inventory, 'instances', instance.id)
  374. # Inventory: Group by region
  375. if self.group_by_region:
  376. self.push(self.inventory, region, dest)
  377. if self.nested_groups:
  378. self.push_group(self.inventory, 'regions', region)
  379. # Inventory: Group by availability zone
  380. if self.group_by_availability_zone:
  381. self.push(self.inventory, instance.placement, dest)
  382. if self.nested_groups:
  383. if self.group_by_region:
  384. self.push_group(self.inventory, region, instance.placement)
  385. self.push_group(self.inventory, 'zones', instance.placement)
  386. # Inventory: Group by Amazon Machine Image (AMI) ID
  387. if self.group_by_ami_id:
  388. ami_id = self.to_safe(instance.image_id)
  389. self.push(self.inventory, ami_id, dest)
  390. if self.nested_groups:
  391. self.push_group(self.inventory, 'images', ami_id)
  392. # Inventory: Group by instance type
  393. if self.group_by_instance_type:
  394. type_name = self.to_safe('type_' + instance.instance_type)
  395. self.push(self.inventory, type_name, dest)
  396. if self.nested_groups:
  397. self.push_group(self.inventory, 'types', type_name)
  398. # Inventory: Group by key pair
  399. if self.group_by_key_pair and instance.key_name:
  400. key_name = self.to_safe('key_' + instance.key_name)
  401. self.push(self.inventory, key_name, dest)
  402. if self.nested_groups:
  403. self.push_group(self.inventory, 'keys', key_name)
  404. # Inventory: Group by VPC
  405. if self.group_by_vpc_id and instance.vpc_id:
  406. vpc_id_name = self.to_safe('vpc_id_' + instance.vpc_id)
  407. self.push(self.inventory, vpc_id_name, dest)
  408. if self.nested_groups:
  409. self.push_group(self.inventory, 'vpcs', vpc_id_name)
  410. # Inventory: Group by security group
  411. if self.group_by_security_group:
  412. try:
  413. for group in instance.groups:
  414. key = self.to_safe("security_group_" + group.name)
  415. self.push(self.inventory, key, dest)
  416. if self.nested_groups:
  417. self.push_group(self.inventory, 'security_groups', key)
  418. except AttributeError:
  419. print 'Package boto seems a bit older.'
  420. print 'Please upgrade boto >= 2.3.0.'
  421. sys.exit(1)
  422. # Inventory: Group by tag keys
  423. if self.group_by_tag_keys:
  424. for k, v in instance.tags.iteritems():
  425. key = self.to_safe("tag_" + k + "=" + v)
  426. self.push(self.inventory, key, dest)
  427. if self.nested_groups:
  428. self.push_group(self.inventory, 'tags', self.to_safe("tag_" + k))
  429. self.push_group(self.inventory, self.to_safe("tag_" + k), key)
  430. # Inventory: Group by Route53 domain names if enabled
  431. if self.route53_enabled and self.group_by_route53_names:
  432. route53_names = self.get_instance_route53_names(instance)
  433. for name in route53_names:
  434. self.push(self.inventory, name, dest)
  435. if self.nested_groups:
  436. self.push_group(self.inventory, 'route53', name)
  437. # Global Tag: instances without tags
  438. if self.group_by_tag_none and len(instance.tags) == 0:
  439. self.push(self.inventory, 'tag_none', dest)
  440. if self.nested_groups:
  441. self.push_group(self.inventory, 'tags', 'tag_none')
  442. # Global Tag: tag all EC2 instances
  443. self.push(self.inventory, 'ec2', dest)
  444. self.inventory["_meta"]["hostvars"][dest] = self.get_host_info_dict_from_instance(instance)
  445. def add_rds_instance(self, instance, region):
  446. ''' Adds an RDS instance to the inventory and index, as long as it is
  447. addressable '''
  448. # Only want available instances unless all_rds_instances is True
  449. if not self.all_rds_instances and instance.status != 'available':
  450. return
  451. # Select the best destination address
  452. dest = instance.endpoint[0]
  453. if not dest:
  454. # Skip instances we cannot address (e.g. private VPC subnet)
  455. return
  456. # Add to index
  457. self.index[dest] = [region, instance.id]
  458. # Inventory: Group by instance ID (always a group of 1)
  459. if self.group_by_instance_id:
  460. self.inventory[instance.id] = [dest]
  461. if self.nested_groups:
  462. self.push_group(self.inventory, 'instances', instance.id)
  463. # Inventory: Group by region
  464. if self.group_by_region:
  465. self.push(self.inventory, region, dest)
  466. if self.nested_groups:
  467. self.push_group(self.inventory, 'regions', region)
  468. # Inventory: Group by availability zone
  469. if self.group_by_availability_zone:
  470. self.push(self.inventory, instance.availability_zone, dest)
  471. if self.nested_groups:
  472. if self.group_by_region:
  473. self.push_group(self.inventory, region, instance.availability_zone)
  474. self.push_group(self.inventory, 'zones', instance.availability_zone)
  475. # Inventory: Group by instance type
  476. if self.group_by_instance_type:
  477. type_name = self.to_safe('type_' + instance.instance_class)
  478. self.push(self.inventory, type_name, dest)
  479. if self.nested_groups:
  480. self.push_group(self.inventory, 'types', type_name)
  481. # Inventory: Group by VPC
  482. if self.group_by_vpc_id and instance.subnet_group and instance.subnet_group.vpc_id:
  483. vpc_id_name = self.to_safe('vpc_id_' + instance.subnet_group.vpc_id)
  484. self.push(self.inventory, vpc_id_name, dest)
  485. if self.nested_groups:
  486. self.push_group(self.inventory, 'vpcs', vpc_id_name)
  487. # Inventory: Group by security group
  488. if self.group_by_security_group:
  489. try:
  490. if instance.security_group:
  491. key = self.to_safe("security_group_" + instance.security_group.name)
  492. self.push(self.inventory, key, dest)
  493. if self.nested_groups:
  494. self.push_group(self.inventory, 'security_groups', key)
  495. except AttributeError:
  496. print 'Package boto seems a bit older.'
  497. print 'Please upgrade boto >= 2.3.0.'
  498. sys.exit(1)
  499. # Inventory: Group by engine
  500. if self.group_by_rds_engine:
  501. self.push(self.inventory, self.to_safe("rds_" + instance.engine), dest)
  502. if self.nested_groups:
  503. self.push_group(self.inventory, 'rds_engines', self.to_safe("rds_" + instance.engine))
  504. # Inventory: Group by parameter group
  505. if self.group_by_rds_parameter_group:
  506. self.push(self.inventory, self.to_safe("rds_parameter_group_" + instance.parameter_group.name), dest)
  507. if self.nested_groups:
  508. self.push_group(self.inventory, 'rds_parameter_groups', self.to_safe("rds_parameter_group_" + instance.parameter_group.name))
  509. # Global Tag: all RDS instances
  510. self.push(self.inventory, 'rds', dest)
  511. self.inventory["_meta"]["hostvars"][dest] = self.get_host_info_dict_from_instance(instance)
  512. def get_route53_records(self):
  513. ''' Get and store the map of resource records to domain names that
  514. point to them. '''
  515. r53_conn = route53.Route53Connection()
  516. all_zones = r53_conn.get_zones()
  517. route53_zones = [ zone for zone in all_zones if zone.name[:-1]
  518. not in self.route53_excluded_zones ]
  519. self.route53_records = {}
  520. for zone in route53_zones:
  521. rrsets = r53_conn.get_all_rrsets(zone.id)
  522. for record_set in rrsets:
  523. record_name = record_set.name
  524. if record_name.endswith('.'):
  525. record_name = record_name[:-1]
  526. for resource in record_set.resource_records:
  527. self.route53_records.setdefault(resource, set())
  528. self.route53_records[resource].add(record_name)
  529. def get_instance_route53_names(self, instance):
  530. ''' Check if an instance is referenced in the records we have from
  531. Route53. If it is, return the list of domain names pointing to said
  532. instance. If nothing points to it, return an empty list. '''
  533. instance_attributes = [ 'public_dns_name', 'private_dns_name',
  534. 'ip_address', 'private_ip_address' ]
  535. name_list = set()
  536. for attrib in instance_attributes:
  537. try:
  538. value = getattr(instance, attrib)
  539. except AttributeError:
  540. continue
  541. if value in self.route53_records:
  542. name_list.update(self.route53_records[value])
  543. return list(name_list)
  544. def get_host_info_dict_from_instance(self, instance):
  545. instance_vars = {}
  546. for key in vars(instance):
  547. value = getattr(instance, key)
  548. key = self.to_safe('ec2_' + key)
  549. # Handle complex types
  550. # state/previous_state changed to properties in boto in https://github.com/boto/boto/commit/a23c379837f698212252720d2af8dec0325c9518
  551. if key == 'ec2__state':
  552. instance_vars['ec2_state'] = instance.state or ''
  553. instance_vars['ec2_state_code'] = instance.state_code
  554. elif key == 'ec2__previous_state':
  555. instance_vars['ec2_previous_state'] = instance.previous_state or ''
  556. instance_vars['ec2_previous_state_code'] = instance.previous_state_code
  557. elif type(value) in [int, bool]:
  558. instance_vars[key] = value
  559. elif type(value) in [str, unicode]:
  560. instance_vars[key] = value.strip()
  561. elif type(value) == type(None):
  562. instance_vars[key] = ''
  563. elif key == 'ec2_region':
  564. instance_vars[key] = value.name
  565. elif key == 'ec2__placement':
  566. instance_vars['ec2_placement'] = value.zone
  567. elif key == 'ec2_tags':
  568. for k, v in value.iteritems():
  569. key = self.to_safe('ec2_tag_' + k)
  570. instance_vars[key] = v
  571. elif key == 'ec2_groups':
  572. group_ids = []
  573. group_names = []
  574. for group in value:
  575. group_ids.append(group.id)
  576. group_names.append(group.name)
  577. instance_vars["ec2_security_group_ids"] = ','.join([str(i) for i in group_ids])
  578. instance_vars["ec2_security_group_names"] = ','.join([str(i) for i in group_names])
  579. else:
  580. pass
  581. # TODO Product codes if someone finds them useful
  582. #print key
  583. #print type(value)
  584. #print value
  585. return instance_vars
  586. def get_host_info(self):
  587. ''' Get variables about a specific host '''
  588. if len(self.index) == 0:
  589. # Need to load index from cache
  590. self.load_index_from_cache()
  591. if not self.args.host in self.index:
  592. # try updating the cache
  593. self.do_api_calls_update_cache()
  594. if not self.args.host in self.index:
  595. # host might not exist anymore
  596. return self.json_format_dict({}, True)
  597. (region, instance_id) = self.index[self.args.host]
  598. instance = self.get_instance(region, instance_id)
  599. return self.json_format_dict(self.get_host_info_dict_from_instance(instance), True)
  600. def push(self, my_dict, key, element):
  601. ''' Push an element onto an array that may not have been defined in
  602. the dict '''
  603. group_info = my_dict.setdefault(key, [])
  604. if isinstance(group_info, dict):
  605. host_list = group_info.setdefault('hosts', [])
  606. host_list.append(element)
  607. else:
  608. group_info.append(element)
  609. def push_group(self, my_dict, key, element):
  610. ''' Push a group as a child of another group. '''
  611. parent_group = my_dict.setdefault(key, {})
  612. if not isinstance(parent_group, dict):
  613. parent_group = my_dict[key] = {'hosts': parent_group}
  614. child_groups = parent_group.setdefault('children', [])
  615. if element not in child_groups:
  616. child_groups.append(element)
  617. def get_inventory_from_cache(self):
  618. ''' Reads the inventory from the cache file and returns it as a JSON
  619. object '''
  620. cache = open(self.cache_path_cache, 'r')
  621. json_inventory = cache.read()
  622. return json_inventory
  623. def load_index_from_cache(self):
  624. ''' Reads the index from the cache file sets self.index '''
  625. cache = open(self.cache_path_index, 'r')
  626. json_index = cache.read()
  627. self.index = json.loads(json_index)
  628. def write_to_cache(self, data, filename):
  629. ''' Writes data in JSON format to a file '''
  630. json_data = self.json_format_dict(data, True)
  631. cache = open(filename, 'w')
  632. cache.write(json_data)
  633. cache.close()
  634. def to_safe(self, word):
  635. ''' Converts 'bad' characters in a string to underscores so they can be
  636. used as Ansible groups '''
  637. return re.sub("[^A-Za-z0-9\-]", "_", word)
  638. def json_format_dict(self, data, pretty=False):
  639. ''' Converts a dict to a JSON object and dumps it as a formatted
  640. string '''
  641. if pretty:
  642. return json.dumps(data, sort_keys=True, indent=2)
  643. else:
  644. return json.dumps(data)
  645. # Run the script
  646. Ec2Inventory()