ec2.py 62 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511
  1. #!/usr/bin/env python2
  2. # pylint: skip-file
  3. '''
  4. EC2 external inventory script
  5. =================================
  6. Generates inventory that Ansible can understand by making API request to
  7. AWS EC2 using the Boto library.
  8. NOTE: This script assumes Ansible is being executed where the environment
  9. variables needed for Boto have already been set:
  10. export AWS_ACCESS_KEY_ID='AK123'
  11. export AWS_SECRET_ACCESS_KEY='abc123'
  12. This script also assumes there is an ec2.ini file alongside it. To specify a
  13. different path to ec2.ini, define the EC2_INI_PATH environment variable:
  14. export EC2_INI_PATH=/path/to/my_ec2.ini
  15. If you're using eucalyptus you need to set the above variables and
  16. you need to define:
  17. export EC2_URL=http://hostname_of_your_cc:port/services/Eucalyptus
  18. If you're using boto profiles (requires boto>=2.24.0) you can choose a profile
  19. using the --boto-profile command line argument (e.g. ec2.py --boto-profile prod) or using
  20. the AWS_PROFILE variable:
  21. AWS_PROFILE=prod ansible-playbook -i ec2.py myplaybook.yml
  22. For more details, see: http://docs.pythonboto.org/en/latest/boto_config_tut.html
  23. When run against a specific host, this script returns the following variables:
  24. - ec2_ami_launch_index
  25. - ec2_architecture
  26. - ec2_association
  27. - ec2_attachTime
  28. - ec2_attachment
  29. - ec2_attachmentId
  30. - ec2_block_devices
  31. - ec2_client_token
  32. - ec2_deleteOnTermination
  33. - ec2_description
  34. - ec2_deviceIndex
  35. - ec2_dns_name
  36. - ec2_eventsSet
  37. - ec2_group_name
  38. - ec2_hypervisor
  39. - ec2_id
  40. - ec2_image_id
  41. - ec2_instanceState
  42. - ec2_instance_type
  43. - ec2_ipOwnerId
  44. - ec2_ip_address
  45. - ec2_item
  46. - ec2_kernel
  47. - ec2_key_name
  48. - ec2_launch_time
  49. - ec2_monitored
  50. - ec2_monitoring
  51. - ec2_networkInterfaceId
  52. - ec2_ownerId
  53. - ec2_persistent
  54. - ec2_placement
  55. - ec2_platform
  56. - ec2_previous_state
  57. - ec2_private_dns_name
  58. - ec2_private_ip_address
  59. - ec2_publicIp
  60. - ec2_public_dns_name
  61. - ec2_ramdisk
  62. - ec2_reason
  63. - ec2_region
  64. - ec2_requester_id
  65. - ec2_root_device_name
  66. - ec2_root_device_type
  67. - ec2_security_group_ids
  68. - ec2_security_group_names
  69. - ec2_shutdown_state
  70. - ec2_sourceDestCheck
  71. - ec2_spot_instance_request_id
  72. - ec2_state
  73. - ec2_state_code
  74. - ec2_state_reason
  75. - ec2_status
  76. - ec2_subnet_id
  77. - ec2_tenancy
  78. - ec2_virtualization_type
  79. - ec2_vpc_id
  80. These variables are pulled out of a boto.ec2.instance object. There is a lack of
  81. consistency with variable spellings (camelCase and underscores) since this
  82. just loops through all variables the object exposes. It is preferred to use the
  83. ones with underscores when multiple exist.
  84. In addition, if an instance has AWS Tags associated with it, each tag is a new
  85. variable named:
  86. - ec2_tag_[Key] = [Value]
  87. Security groups are comma-separated in 'ec2_security_group_ids' and
  88. 'ec2_security_group_names'.
  89. '''
  90. # (c) 2012, Peter Sankauskas
  91. #
  92. # This file is part of Ansible,
  93. #
  94. # Ansible is free software: you can redistribute it and/or modify
  95. # it under the terms of the GNU General Public License as published by
  96. # the Free Software Foundation, either version 3 of the License, or
  97. # (at your option) any later version.
  98. #
  99. # Ansible is distributed in the hope that it will be useful,
  100. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  101. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  102. # GNU General Public License for more details.
  103. #
  104. # You should have received a copy of the GNU General Public License
  105. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
  106. ######################################################################
  107. import sys
  108. import os
  109. import argparse
  110. import re
  111. from time import time
  112. import boto
  113. from boto import ec2
  114. from boto import rds
  115. from boto import elasticache
  116. from boto import route53
  117. import six
  118. from ansible.module_utils import ec2 as ec2_utils
  119. HAS_BOTO3 = False
  120. try:
  121. import boto3
  122. HAS_BOTO3 = True
  123. except ImportError:
  124. pass
  125. from six.moves import configparser
  126. from collections import defaultdict
  127. try:
  128. import json
  129. except ImportError:
  130. import simplejson as json
  131. class Ec2Inventory(object):
  132. def _empty_inventory(self):
  133. return {"_meta" : {"hostvars" : {}}}
  134. def __init__(self):
  135. ''' Main execution path '''
  136. # Inventory grouped by instance IDs, tags, security groups, regions,
  137. # and availability zones
  138. self.inventory = self._empty_inventory()
  139. # Index of hostname (address) to instance ID
  140. self.index = {}
  141. # Boto profile to use (if any)
  142. self.boto_profile = None
  143. # AWS credentials.
  144. self.credentials = {}
  145. # Read settings and parse CLI arguments
  146. self.parse_cli_args()
  147. self.read_settings()
  148. # Make sure that profile_name is not passed at all if not set
  149. # as pre 2.24 boto will fall over otherwise
  150. if self.boto_profile:
  151. if not hasattr(boto.ec2.EC2Connection, 'profile_name'):
  152. self.fail_with_error("boto version must be >= 2.24 to use profile")
  153. # Cache
  154. if self.args.refresh_cache:
  155. self.do_api_calls_update_cache()
  156. elif not self.is_cache_valid():
  157. self.do_api_calls_update_cache()
  158. # Data to print
  159. if self.args.host:
  160. data_to_print = self.get_host_info()
  161. elif self.args.list:
  162. # Display list of instances for inventory
  163. if self.inventory == self._empty_inventory():
  164. data_to_print = self.get_inventory_from_cache()
  165. else:
  166. data_to_print = self.json_format_dict(self.inventory, True)
  167. print(data_to_print)
  168. def is_cache_valid(self):
  169. ''' Determines if the cache files have expired, or if it is still valid '''
  170. if os.path.isfile(self.cache_path_cache):
  171. mod_time = os.path.getmtime(self.cache_path_cache)
  172. current_time = time()
  173. if (mod_time + self.cache_max_age) > current_time:
  174. if os.path.isfile(self.cache_path_index):
  175. return True
  176. return False
  177. def read_settings(self):
  178. ''' Reads the settings from the ec2.ini file '''
  179. if six.PY3:
  180. config = configparser.ConfigParser()
  181. else:
  182. config = configparser.SafeConfigParser()
  183. ec2_default_ini_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ec2.ini')
  184. ec2_ini_path = os.path.expanduser(os.path.expandvars(os.environ.get('EC2_INI_PATH', ec2_default_ini_path)))
  185. config.read(ec2_ini_path)
  186. # is eucalyptus?
  187. self.eucalyptus_host = None
  188. self.eucalyptus = False
  189. if config.has_option('ec2', 'eucalyptus'):
  190. self.eucalyptus = config.getboolean('ec2', 'eucalyptus')
  191. if self.eucalyptus and config.has_option('ec2', 'eucalyptus_host'):
  192. self.eucalyptus_host = config.get('ec2', 'eucalyptus_host')
  193. # Regions
  194. self.regions = []
  195. configRegions = config.get('ec2', 'regions')
  196. configRegions_exclude = config.get('ec2', 'regions_exclude')
  197. if (configRegions == 'all'):
  198. if self.eucalyptus_host:
  199. self.regions.append(boto.connect_euca(host=self.eucalyptus_host).region.name, **self.credentials)
  200. else:
  201. for regionInfo in ec2.regions():
  202. if regionInfo.name not in configRegions_exclude:
  203. self.regions.append(regionInfo.name)
  204. else:
  205. self.regions = configRegions.split(",")
  206. # Destination addresses
  207. self.destination_variable = config.get('ec2', 'destination_variable')
  208. self.vpc_destination_variable = config.get('ec2', 'vpc_destination_variable')
  209. if config.has_option('ec2', 'hostname_variable'):
  210. self.hostname_variable = config.get('ec2', 'hostname_variable')
  211. else:
  212. self.hostname_variable = None
  213. if config.has_option('ec2', 'destination_format') and \
  214. config.has_option('ec2', 'destination_format_tags'):
  215. self.destination_format = config.get('ec2', 'destination_format')
  216. self.destination_format_tags = config.get('ec2', 'destination_format_tags').split(',')
  217. else:
  218. self.destination_format = None
  219. self.destination_format_tags = None
  220. # Route53
  221. self.route53_enabled = config.getboolean('ec2', 'route53')
  222. self.route53_excluded_zones = []
  223. if config.has_option('ec2', 'route53_excluded_zones'):
  224. self.route53_excluded_zones.extend(
  225. config.get('ec2', 'route53_excluded_zones', '').split(','))
  226. # Include RDS instances?
  227. self.rds_enabled = True
  228. if config.has_option('ec2', 'rds'):
  229. self.rds_enabled = config.getboolean('ec2', 'rds')
  230. # Include RDS cluster instances?
  231. if config.has_option('ec2', 'include_rds_clusters'):
  232. self.include_rds_clusters = config.getboolean('ec2', 'include_rds_clusters')
  233. else:
  234. self.include_rds_clusters = False
  235. # Include ElastiCache instances?
  236. self.elasticache_enabled = True
  237. if config.has_option('ec2', 'elasticache'):
  238. self.elasticache_enabled = config.getboolean('ec2', 'elasticache')
  239. # Return all EC2 instances?
  240. if config.has_option('ec2', 'all_instances'):
  241. self.all_instances = config.getboolean('ec2', 'all_instances')
  242. else:
  243. self.all_instances = False
  244. # Instance states to be gathered in inventory. Default is 'running'.
  245. # Setting 'all_instances' to 'yes' overrides this option.
  246. ec2_valid_instance_states = [
  247. 'pending',
  248. 'running',
  249. 'shutting-down',
  250. 'terminated',
  251. 'stopping',
  252. 'stopped'
  253. ]
  254. self.ec2_instance_states = []
  255. if self.all_instances:
  256. self.ec2_instance_states = ec2_valid_instance_states
  257. elif config.has_option('ec2', 'instance_states'):
  258. for instance_state in config.get('ec2', 'instance_states').split(','):
  259. instance_state = instance_state.strip()
  260. if instance_state not in ec2_valid_instance_states:
  261. continue
  262. self.ec2_instance_states.append(instance_state)
  263. else:
  264. self.ec2_instance_states = ['running']
  265. # Return all RDS instances? (if RDS is enabled)
  266. if config.has_option('ec2', 'all_rds_instances') and self.rds_enabled:
  267. self.all_rds_instances = config.getboolean('ec2', 'all_rds_instances')
  268. else:
  269. self.all_rds_instances = False
  270. # Return all ElastiCache replication groups? (if ElastiCache is enabled)
  271. if config.has_option('ec2', 'all_elasticache_replication_groups') and self.elasticache_enabled:
  272. self.all_elasticache_replication_groups = config.getboolean('ec2', 'all_elasticache_replication_groups')
  273. else:
  274. self.all_elasticache_replication_groups = False
  275. # Return all ElastiCache clusters? (if ElastiCache is enabled)
  276. if config.has_option('ec2', 'all_elasticache_clusters') and self.elasticache_enabled:
  277. self.all_elasticache_clusters = config.getboolean('ec2', 'all_elasticache_clusters')
  278. else:
  279. self.all_elasticache_clusters = False
  280. # Return all ElastiCache nodes? (if ElastiCache is enabled)
  281. if config.has_option('ec2', 'all_elasticache_nodes') and self.elasticache_enabled:
  282. self.all_elasticache_nodes = config.getboolean('ec2', 'all_elasticache_nodes')
  283. else:
  284. self.all_elasticache_nodes = False
  285. # boto configuration profile (prefer CLI argument)
  286. self.boto_profile = self.args.boto_profile
  287. if config.has_option('ec2', 'boto_profile') and not self.boto_profile:
  288. self.boto_profile = config.get('ec2', 'boto_profile')
  289. # AWS credentials (prefer environment variables)
  290. if not (self.boto_profile or os.environ.get('AWS_ACCESS_KEY_ID') or
  291. os.environ.get('AWS_PROFILE')):
  292. if config.has_option('credentials', 'aws_access_key_id'):
  293. aws_access_key_id = config.get('credentials', 'aws_access_key_id')
  294. else:
  295. aws_access_key_id = None
  296. if config.has_option('credentials', 'aws_secret_access_key'):
  297. aws_secret_access_key = config.get('credentials', 'aws_secret_access_key')
  298. else:
  299. aws_secret_access_key = None
  300. if config.has_option('credentials', 'aws_security_token'):
  301. aws_security_token = config.get('credentials', 'aws_security_token')
  302. else:
  303. aws_security_token = None
  304. if aws_access_key_id:
  305. self.credentials = {
  306. 'aws_access_key_id': aws_access_key_id,
  307. 'aws_secret_access_key': aws_secret_access_key
  308. }
  309. if aws_security_token:
  310. self.credentials['security_token'] = aws_security_token
  311. # Cache related
  312. cache_dir = os.path.expanduser(config.get('ec2', 'cache_path'))
  313. if self.boto_profile:
  314. cache_dir = os.path.join(cache_dir, 'profile_' + self.boto_profile)
  315. if not os.path.exists(cache_dir):
  316. os.makedirs(cache_dir)
  317. cache_name = 'ansible-ec2'
  318. aws_profile = lambda: (self.boto_profile or
  319. os.environ.get('AWS_PROFILE') or
  320. os.environ.get('AWS_ACCESS_KEY_ID') or
  321. self.credentials.get('aws_access_key_id', None))
  322. if aws_profile():
  323. cache_name = '%s-%s' % (cache_name, aws_profile())
  324. self.cache_path_cache = cache_dir + "/%s.cache" % cache_name
  325. self.cache_path_index = cache_dir + "/%s.index" % cache_name
  326. self.cache_max_age = config.getint('ec2', 'cache_max_age')
  327. if config.has_option('ec2', 'expand_csv_tags'):
  328. self.expand_csv_tags = config.getboolean('ec2', 'expand_csv_tags')
  329. else:
  330. self.expand_csv_tags = False
  331. # Configure nested groups instead of flat namespace.
  332. if config.has_option('ec2', 'nested_groups'):
  333. self.nested_groups = config.getboolean('ec2', 'nested_groups')
  334. else:
  335. self.nested_groups = False
  336. # Replace dash or not in group names
  337. if config.has_option('ec2', 'replace_dash_in_groups'):
  338. self.replace_dash_in_groups = config.getboolean('ec2', 'replace_dash_in_groups')
  339. else:
  340. self.replace_dash_in_groups = True
  341. # Configure which groups should be created.
  342. group_by_options = [
  343. 'group_by_instance_id',
  344. 'group_by_region',
  345. 'group_by_availability_zone',
  346. 'group_by_ami_id',
  347. 'group_by_instance_type',
  348. 'group_by_key_pair',
  349. 'group_by_vpc_id',
  350. 'group_by_security_group',
  351. 'group_by_tag_keys',
  352. 'group_by_tag_none',
  353. 'group_by_route53_names',
  354. 'group_by_rds_engine',
  355. 'group_by_rds_parameter_group',
  356. 'group_by_elasticache_engine',
  357. 'group_by_elasticache_cluster',
  358. 'group_by_elasticache_parameter_group',
  359. 'group_by_elasticache_replication_group',
  360. ]
  361. for option in group_by_options:
  362. if config.has_option('ec2', option):
  363. setattr(self, option, config.getboolean('ec2', option))
  364. else:
  365. setattr(self, option, True)
  366. # Do we need to just include hosts that match a pattern?
  367. try:
  368. pattern_include = config.get('ec2', 'pattern_include')
  369. if pattern_include and len(pattern_include) > 0:
  370. self.pattern_include = re.compile(pattern_include)
  371. else:
  372. self.pattern_include = None
  373. except configparser.NoOptionError:
  374. self.pattern_include = None
  375. # Do we need to exclude hosts that match a pattern?
  376. try:
  377. pattern_exclude = config.get('ec2', 'pattern_exclude');
  378. if pattern_exclude and len(pattern_exclude) > 0:
  379. self.pattern_exclude = re.compile(pattern_exclude)
  380. else:
  381. self.pattern_exclude = None
  382. except configparser.NoOptionError:
  383. self.pattern_exclude = None
  384. # Instance filters (see boto and EC2 API docs). Ignore invalid filters.
  385. self.ec2_instance_filters = defaultdict(list)
  386. if config.has_option('ec2', 'instance_filters'):
  387. filters = [f for f in config.get('ec2', 'instance_filters').split(',') if f]
  388. for instance_filter in filters:
  389. instance_filter = instance_filter.strip()
  390. if not instance_filter or '=' not in instance_filter:
  391. continue
  392. filter_key, filter_value = [x.strip() for x in instance_filter.split('=', 1)]
  393. if not filter_key:
  394. continue
  395. self.ec2_instance_filters[filter_key].append(filter_value)
  396. def parse_cli_args(self):
  397. ''' Command line argument processing '''
  398. parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on EC2')
  399. parser.add_argument('--list', action='store_true', default=True,
  400. help='List instances (default: True)')
  401. parser.add_argument('--host', action='store',
  402. help='Get all the variables about a specific instance')
  403. parser.add_argument('--refresh-cache', action='store_true', default=False,
  404. help='Force refresh of cache by making API requests to EC2 (default: False - use cache files)')
  405. parser.add_argument('--profile', '--boto-profile', action='store', dest='boto_profile',
  406. help='Use boto profile for connections to EC2')
  407. self.args = parser.parse_args()
  408. def do_api_calls_update_cache(self):
  409. ''' Do API calls to each region, and save data in cache files '''
  410. if self.route53_enabled:
  411. self.get_route53_records()
  412. for region in self.regions:
  413. self.get_instances_by_region(region)
  414. if self.rds_enabled:
  415. self.get_rds_instances_by_region(region)
  416. if self.elasticache_enabled:
  417. self.get_elasticache_clusters_by_region(region)
  418. self.get_elasticache_replication_groups_by_region(region)
  419. if self.include_rds_clusters:
  420. self.include_rds_clusters_by_region(region)
  421. self.write_to_cache(self.inventory, self.cache_path_cache)
  422. self.write_to_cache(self.index, self.cache_path_index)
  423. def connect(self, region):
  424. ''' create connection to api server'''
  425. if self.eucalyptus:
  426. conn = boto.connect_euca(host=self.eucalyptus_host, **self.credentials)
  427. conn.APIVersion = '2010-08-31'
  428. else:
  429. conn = self.connect_to_aws(ec2, region)
  430. return conn
  431. def boto_fix_security_token_in_profile(self, connect_args):
  432. ''' monkey patch for boto issue boto/boto#2100 '''
  433. profile = 'profile ' + self.boto_profile
  434. if boto.config.has_option(profile, 'aws_security_token'):
  435. connect_args['security_token'] = boto.config.get(profile, 'aws_security_token')
  436. return connect_args
  437. def connect_to_aws(self, module, region):
  438. connect_args = self.credentials
  439. # only pass the profile name if it's set (as it is not supported by older boto versions)
  440. if self.boto_profile:
  441. connect_args['profile_name'] = self.boto_profile
  442. self.boto_fix_security_token_in_profile(connect_args)
  443. conn = module.connect_to_region(region, **connect_args)
  444. # connect_to_region will fail "silently" by returning None if the region name is wrong or not supported
  445. if conn is None:
  446. self.fail_with_error("region name: %s likely not supported, or AWS is down. connection to region failed." % region)
  447. return conn
  448. def get_instances_by_region(self, region):
  449. ''' Makes an AWS EC2 API call to the list of instances in a particular
  450. region '''
  451. try:
  452. conn = self.connect(region)
  453. reservations = []
  454. if self.ec2_instance_filters:
  455. for filter_key, filter_values in self.ec2_instance_filters.items():
  456. reservations.extend(conn.get_all_instances(filters = { filter_key : filter_values }))
  457. else:
  458. reservations = conn.get_all_instances()
  459. # Pull the tags back in a second step
  460. # AWS are on record as saying that the tags fetched in the first `get_all_instances` request are not
  461. # reliable and may be missing, and the only way to guarantee they are there is by calling `get_all_tags`
  462. instance_ids = []
  463. for reservation in reservations:
  464. instance_ids.extend([instance.id for instance in reservation.instances])
  465. max_filter_value = 199
  466. tags = []
  467. for i in range(0, len(instance_ids), max_filter_value):
  468. tags.extend(conn.get_all_tags(filters={'resource-type': 'instance', 'resource-id': instance_ids[i:i+max_filter_value]}))
  469. tags_by_instance_id = defaultdict(dict)
  470. for tag in tags:
  471. tags_by_instance_id[tag.res_id][tag.name] = tag.value
  472. for reservation in reservations:
  473. for instance in reservation.instances:
  474. instance.tags = tags_by_instance_id[instance.id]
  475. self.add_instance(instance, region)
  476. except boto.exception.BotoServerError as e:
  477. if e.error_code == 'AuthFailure':
  478. error = self.get_auth_error_message()
  479. else:
  480. backend = 'Eucalyptus' if self.eucalyptus else 'AWS'
  481. error = "Error connecting to %s backend.\n%s" % (backend, e.message)
  482. self.fail_with_error(error, 'getting EC2 instances')
  483. def get_rds_instances_by_region(self, region):
  484. ''' Makes an AWS API call to the list of RDS instances in a particular
  485. region '''
  486. try:
  487. conn = self.connect_to_aws(rds, region)
  488. if conn:
  489. marker = None
  490. while True:
  491. instances = conn.get_all_dbinstances(marker=marker)
  492. marker = instances.marker
  493. for instance in instances:
  494. self.add_rds_instance(instance, region)
  495. if not marker:
  496. break
  497. except boto.exception.BotoServerError as e:
  498. error = e.reason
  499. if e.error_code == 'AuthFailure':
  500. error = self.get_auth_error_message()
  501. if not e.reason == "Forbidden":
  502. error = "Looks like AWS RDS is down:\n%s" % e.message
  503. self.fail_with_error(error, 'getting RDS instances')
  504. def include_rds_clusters_by_region(self, region):
  505. if not HAS_BOTO3:
  506. self.fail_with_error("Working with RDS clusters requires boto3 - please install boto3 and try again",
  507. "getting RDS clusters")
  508. client = ec2_utils.boto3_inventory_conn('client', 'rds', region, **self.credentials)
  509. marker, clusters = '', []
  510. while marker is not None:
  511. resp = client.describe_db_clusters(Marker=marker)
  512. clusters.extend(resp["DBClusters"])
  513. marker = resp.get('Marker', None)
  514. account_id = boto.connect_iam().get_user().arn.split(':')[4]
  515. c_dict = {}
  516. for c in clusters:
  517. # remove these datetime objects as there is no serialisation to json
  518. # currently in place and we don't need the data yet
  519. if 'EarliestRestorableTime' in c:
  520. del c['EarliestRestorableTime']
  521. if 'LatestRestorableTime' in c:
  522. del c['LatestRestorableTime']
  523. if self.ec2_instance_filters == {}:
  524. matches_filter = True
  525. else:
  526. matches_filter = False
  527. try:
  528. # arn:aws:rds:<region>:<account number>:<resourcetype>:<name>
  529. tags = client.list_tags_for_resource(
  530. ResourceName='arn:aws:rds:' + region + ':' + account_id + ':cluster:' + c['DBClusterIdentifier'])
  531. c['Tags'] = tags['TagList']
  532. if self.ec2_instance_filters:
  533. for filter_key, filter_values in self.ec2_instance_filters.items():
  534. # get AWS tag key e.g. tag:env will be 'env'
  535. tag_name = filter_key.split(":", 1)[1]
  536. # Filter values is a list (if you put multiple values for the same tag name)
  537. matches_filter = any(d['Key'] == tag_name and d['Value'] in filter_values for d in c['Tags'])
  538. if matches_filter:
  539. # it matches a filter, so stop looking for further matches
  540. break
  541. except Exception as e:
  542. if e.message.find('DBInstanceNotFound') >= 0:
  543. # AWS RDS bug (2016-01-06) means deletion does not fully complete and leave an 'empty' cluster.
  544. # Ignore errors when trying to find tags for these
  545. pass
  546. # ignore empty clusters caused by AWS bug
  547. if len(c['DBClusterMembers']) == 0:
  548. continue
  549. elif matches_filter:
  550. c_dict[c['DBClusterIdentifier']] = c
  551. self.inventory['db_clusters'] = c_dict
  552. def get_elasticache_clusters_by_region(self, region):
  553. ''' Makes an AWS API call to the list of ElastiCache clusters (with
  554. nodes' info) in a particular region.'''
  555. # ElastiCache boto module doesn't provide a get_all_intances method,
  556. # that's why we need to call describe directly (it would be called by
  557. # the shorthand method anyway...)
  558. try:
  559. conn = self.connect_to_aws(elasticache, region)
  560. if conn:
  561. # show_cache_node_info = True
  562. # because we also want nodes' information
  563. response = conn.describe_cache_clusters(None, None, None, True)
  564. except boto.exception.BotoServerError as e:
  565. error = e.reason
  566. if e.error_code == 'AuthFailure':
  567. error = self.get_auth_error_message()
  568. if not e.reason == "Forbidden":
  569. error = "Looks like AWS ElastiCache is down:\n%s" % e.message
  570. self.fail_with_error(error, 'getting ElastiCache clusters')
  571. try:
  572. # Boto also doesn't provide wrapper classes to CacheClusters or
  573. # CacheNodes. Because of that we can't make use of the get_list
  574. # method in the AWSQueryConnection. Let's do the work manually
  575. clusters = response['DescribeCacheClustersResponse']['DescribeCacheClustersResult']['CacheClusters']
  576. except KeyError as e:
  577. error = "ElastiCache query to AWS failed (unexpected format)."
  578. self.fail_with_error(error, 'getting ElastiCache clusters')
  579. for cluster in clusters:
  580. self.add_elasticache_cluster(cluster, region)
  581. def get_elasticache_replication_groups_by_region(self, region):
  582. ''' Makes an AWS API call to the list of ElastiCache replication groups
  583. in a particular region.'''
  584. # ElastiCache boto module doesn't provide a get_all_intances method,
  585. # that's why we need to call describe directly (it would be called by
  586. # the shorthand method anyway...)
  587. try:
  588. conn = self.connect_to_aws(elasticache, region)
  589. if conn:
  590. response = conn.describe_replication_groups()
  591. except boto.exception.BotoServerError as e:
  592. error = e.reason
  593. if e.error_code == 'AuthFailure':
  594. error = self.get_auth_error_message()
  595. if not e.reason == "Forbidden":
  596. error = "Looks like AWS ElastiCache [Replication Groups] is down:\n%s" % e.message
  597. self.fail_with_error(error, 'getting ElastiCache clusters')
  598. try:
  599. # Boto also doesn't provide wrapper classes to ReplicationGroups
  600. # Because of that we can't make use of the get_list method in the
  601. # AWSQueryConnection. Let's do the work manually
  602. replication_groups = response['DescribeReplicationGroupsResponse']['DescribeReplicationGroupsResult']['ReplicationGroups']
  603. except KeyError as e:
  604. error = "ElastiCache [Replication Groups] query to AWS failed (unexpected format)."
  605. self.fail_with_error(error, 'getting ElastiCache clusters')
  606. for replication_group in replication_groups:
  607. self.add_elasticache_replication_group(replication_group, region)
  608. def get_auth_error_message(self):
  609. ''' create an informative error message if there is an issue authenticating'''
  610. errors = ["Authentication error retrieving ec2 inventory."]
  611. if None in [os.environ.get('AWS_ACCESS_KEY_ID'), os.environ.get('AWS_SECRET_ACCESS_KEY')]:
  612. errors.append(' - No AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY environment vars found')
  613. else:
  614. errors.append(' - AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment vars found but may not be correct')
  615. boto_paths = ['/etc/boto.cfg', '~/.boto', '~/.aws/credentials']
  616. boto_config_found = list(p for p in boto_paths if os.path.isfile(os.path.expanduser(p)))
  617. if len(boto_config_found) > 0:
  618. errors.append(" - Boto configs found at '%s', but the credentials contained may not be correct" % ', '.join(boto_config_found))
  619. else:
  620. errors.append(" - No Boto config found at any expected location '%s'" % ', '.join(boto_paths))
  621. return '\n'.join(errors)
  622. def fail_with_error(self, err_msg, err_operation=None):
  623. '''log an error to std err for ansible-playbook to consume and exit'''
  624. if err_operation:
  625. err_msg = 'ERROR: "{err_msg}", while: {err_operation}'.format(
  626. err_msg=err_msg, err_operation=err_operation)
  627. sys.stderr.write(err_msg)
  628. sys.exit(1)
  629. def get_instance(self, region, instance_id):
  630. conn = self.connect(region)
  631. reservations = conn.get_all_instances([instance_id])
  632. for reservation in reservations:
  633. for instance in reservation.instances:
  634. return instance
  635. def add_instance(self, instance, region):
  636. ''' Adds an instance to the inventory and index, as long as it is
  637. addressable '''
  638. # Only return instances with desired instance states
  639. if instance.state not in self.ec2_instance_states:
  640. return
  641. # Select the best destination address
  642. if self.destination_format and self.destination_format_tags:
  643. dest = self.destination_format.format(*[ getattr(instance, 'tags').get(tag, '') for tag in self.destination_format_tags ])
  644. elif instance.subnet_id:
  645. dest = getattr(instance, self.vpc_destination_variable, None)
  646. if dest is None:
  647. dest = getattr(instance, 'tags').get(self.vpc_destination_variable, None)
  648. else:
  649. dest = getattr(instance, self.destination_variable, None)
  650. if dest is None:
  651. dest = getattr(instance, 'tags').get(self.destination_variable, None)
  652. if not dest:
  653. # Skip instances we cannot address (e.g. private VPC subnet)
  654. return
  655. # Set the inventory name
  656. hostname = None
  657. if self.hostname_variable:
  658. if self.hostname_variable.startswith('tag_'):
  659. hostname = instance.tags.get(self.hostname_variable[4:], None)
  660. else:
  661. hostname = getattr(instance, self.hostname_variable)
  662. # If we can't get a nice hostname, use the destination address
  663. if not hostname:
  664. hostname = dest
  665. else:
  666. hostname = self.to_safe(hostname).lower()
  667. # if we only want to include hosts that match a pattern, skip those that don't
  668. if self.pattern_include and not self.pattern_include.match(hostname):
  669. return
  670. # if we need to exclude hosts that match a pattern, skip those
  671. if self.pattern_exclude and self.pattern_exclude.match(hostname):
  672. return
  673. # Add to index
  674. self.index[hostname] = [region, instance.id]
  675. # Inventory: Group by instance ID (always a group of 1)
  676. if self.group_by_instance_id:
  677. self.inventory[instance.id] = [hostname]
  678. if self.nested_groups:
  679. self.push_group(self.inventory, 'instances', instance.id)
  680. # Inventory: Group by region
  681. if self.group_by_region:
  682. self.push(self.inventory, region, hostname)
  683. if self.nested_groups:
  684. self.push_group(self.inventory, 'regions', region)
  685. # Inventory: Group by availability zone
  686. if self.group_by_availability_zone:
  687. self.push(self.inventory, instance.placement, hostname)
  688. if self.nested_groups:
  689. if self.group_by_region:
  690. self.push_group(self.inventory, region, instance.placement)
  691. self.push_group(self.inventory, 'zones', instance.placement)
  692. # Inventory: Group by Amazon Machine Image (AMI) ID
  693. if self.group_by_ami_id:
  694. ami_id = self.to_safe(instance.image_id)
  695. self.push(self.inventory, ami_id, hostname)
  696. if self.nested_groups:
  697. self.push_group(self.inventory, 'images', ami_id)
  698. # Inventory: Group by instance type
  699. if self.group_by_instance_type:
  700. type_name = self.to_safe('type_' + instance.instance_type)
  701. self.push(self.inventory, type_name, hostname)
  702. if self.nested_groups:
  703. self.push_group(self.inventory, 'types', type_name)
  704. # Inventory: Group by key pair
  705. if self.group_by_key_pair and instance.key_name:
  706. key_name = self.to_safe('key_' + instance.key_name)
  707. self.push(self.inventory, key_name, hostname)
  708. if self.nested_groups:
  709. self.push_group(self.inventory, 'keys', key_name)
  710. # Inventory: Group by VPC
  711. if self.group_by_vpc_id and instance.vpc_id:
  712. vpc_id_name = self.to_safe('vpc_id_' + instance.vpc_id)
  713. self.push(self.inventory, vpc_id_name, hostname)
  714. if self.nested_groups:
  715. self.push_group(self.inventory, 'vpcs', vpc_id_name)
  716. # Inventory: Group by security group
  717. if self.group_by_security_group:
  718. try:
  719. for group in instance.groups:
  720. key = self.to_safe("security_group_" + group.name)
  721. self.push(self.inventory, key, hostname)
  722. if self.nested_groups:
  723. self.push_group(self.inventory, 'security_groups', key)
  724. except AttributeError:
  725. self.fail_with_error('\n'.join(['Package boto seems a bit older.',
  726. 'Please upgrade boto >= 2.3.0.']))
  727. # Inventory: Group by tag keys
  728. if self.group_by_tag_keys:
  729. for k, v in instance.tags.items():
  730. if self.expand_csv_tags and v and ',' in v:
  731. values = map(lambda x: x.strip(), v.split(','))
  732. else:
  733. values = [v]
  734. for v in values:
  735. if v:
  736. key = self.to_safe("tag_" + k + "=" + v)
  737. else:
  738. key = self.to_safe("tag_" + k)
  739. self.push(self.inventory, key, hostname)
  740. if self.nested_groups:
  741. self.push_group(self.inventory, 'tags', self.to_safe("tag_" + k))
  742. if v:
  743. self.push_group(self.inventory, self.to_safe("tag_" + k), key)
  744. # Inventory: Group by Route53 domain names if enabled
  745. if self.route53_enabled and self.group_by_route53_names:
  746. route53_names = self.get_instance_route53_names(instance)
  747. for name in route53_names:
  748. self.push(self.inventory, name, hostname)
  749. if self.nested_groups:
  750. self.push_group(self.inventory, 'route53', name)
  751. # Global Tag: instances without tags
  752. if self.group_by_tag_none and len(instance.tags) == 0:
  753. self.push(self.inventory, 'tag_none', hostname)
  754. if self.nested_groups:
  755. self.push_group(self.inventory, 'tags', 'tag_none')
  756. # Global Tag: tag all EC2 instances
  757. self.push(self.inventory, 'ec2', hostname)
  758. self.inventory["_meta"]["hostvars"][hostname] = self.get_host_info_dict_from_instance(instance)
  759. self.inventory["_meta"]["hostvars"][hostname]['ansible_ssh_host'] = dest
  760. def add_rds_instance(self, instance, region):
  761. ''' Adds an RDS instance to the inventory and index, as long as it is
  762. addressable '''
  763. # Only want available instances unless all_rds_instances is True
  764. if not self.all_rds_instances and instance.status != 'available':
  765. return
  766. # Select the best destination address
  767. dest = instance.endpoint[0]
  768. if not dest:
  769. # Skip instances we cannot address (e.g. private VPC subnet)
  770. return
  771. # Set the inventory name
  772. hostname = None
  773. if self.hostname_variable:
  774. if self.hostname_variable.startswith('tag_'):
  775. hostname = instance.tags.get(self.hostname_variable[4:], None)
  776. else:
  777. hostname = getattr(instance, self.hostname_variable)
  778. # If we can't get a nice hostname, use the destination address
  779. if not hostname:
  780. hostname = dest
  781. hostname = self.to_safe(hostname).lower()
  782. # Add to index
  783. self.index[hostname] = [region, instance.id]
  784. # Inventory: Group by instance ID (always a group of 1)
  785. if self.group_by_instance_id:
  786. self.inventory[instance.id] = [hostname]
  787. if self.nested_groups:
  788. self.push_group(self.inventory, 'instances', instance.id)
  789. # Inventory: Group by region
  790. if self.group_by_region:
  791. self.push(self.inventory, region, hostname)
  792. if self.nested_groups:
  793. self.push_group(self.inventory, 'regions', region)
  794. # Inventory: Group by availability zone
  795. if self.group_by_availability_zone:
  796. self.push(self.inventory, instance.availability_zone, hostname)
  797. if self.nested_groups:
  798. if self.group_by_region:
  799. self.push_group(self.inventory, region, instance.availability_zone)
  800. self.push_group(self.inventory, 'zones', instance.availability_zone)
  801. # Inventory: Group by instance type
  802. if self.group_by_instance_type:
  803. type_name = self.to_safe('type_' + instance.instance_class)
  804. self.push(self.inventory, type_name, hostname)
  805. if self.nested_groups:
  806. self.push_group(self.inventory, 'types', type_name)
  807. # Inventory: Group by VPC
  808. if self.group_by_vpc_id and instance.subnet_group and instance.subnet_group.vpc_id:
  809. vpc_id_name = self.to_safe('vpc_id_' + instance.subnet_group.vpc_id)
  810. self.push(self.inventory, vpc_id_name, hostname)
  811. if self.nested_groups:
  812. self.push_group(self.inventory, 'vpcs', vpc_id_name)
  813. # Inventory: Group by security group
  814. if self.group_by_security_group:
  815. try:
  816. if instance.security_group:
  817. key = self.to_safe("security_group_" + instance.security_group.name)
  818. self.push(self.inventory, key, hostname)
  819. if self.nested_groups:
  820. self.push_group(self.inventory, 'security_groups', key)
  821. except AttributeError:
  822. self.fail_with_error('\n'.join(['Package boto seems a bit older.',
  823. 'Please upgrade boto >= 2.3.0.']))
  824. # Inventory: Group by engine
  825. if self.group_by_rds_engine:
  826. self.push(self.inventory, self.to_safe("rds_" + instance.engine), hostname)
  827. if self.nested_groups:
  828. self.push_group(self.inventory, 'rds_engines', self.to_safe("rds_" + instance.engine))
  829. # Inventory: Group by parameter group
  830. if self.group_by_rds_parameter_group:
  831. self.push(self.inventory, self.to_safe("rds_parameter_group_" + instance.parameter_group.name), hostname)
  832. if self.nested_groups:
  833. self.push_group(self.inventory, 'rds_parameter_groups', self.to_safe("rds_parameter_group_" + instance.parameter_group.name))
  834. # Global Tag: all RDS instances
  835. self.push(self.inventory, 'rds', hostname)
  836. self.inventory["_meta"]["hostvars"][hostname] = self.get_host_info_dict_from_instance(instance)
  837. self.inventory["_meta"]["hostvars"][hostname]['ansible_ssh_host'] = dest
  838. def add_elasticache_cluster(self, cluster, region):
  839. ''' Adds an ElastiCache cluster to the inventory and index, as long as
  840. it's nodes are addressable '''
  841. # Only want available clusters unless all_elasticache_clusters is True
  842. if not self.all_elasticache_clusters and cluster['CacheClusterStatus'] != 'available':
  843. return
  844. # Select the best destination address
  845. if 'ConfigurationEndpoint' in cluster and cluster['ConfigurationEndpoint']:
  846. # Memcached cluster
  847. dest = cluster['ConfigurationEndpoint']['Address']
  848. is_redis = False
  849. else:
  850. # Redis sigle node cluster
  851. # Because all Redis clusters are single nodes, we'll merge the
  852. # info from the cluster with info about the node
  853. dest = cluster['CacheNodes'][0]['Endpoint']['Address']
  854. is_redis = True
  855. if not dest:
  856. # Skip clusters we cannot address (e.g. private VPC subnet)
  857. return
  858. # Add to index
  859. self.index[dest] = [region, cluster['CacheClusterId']]
  860. # Inventory: Group by instance ID (always a group of 1)
  861. if self.group_by_instance_id:
  862. self.inventory[cluster['CacheClusterId']] = [dest]
  863. if self.nested_groups:
  864. self.push_group(self.inventory, 'instances', cluster['CacheClusterId'])
  865. # Inventory: Group by region
  866. if self.group_by_region and not is_redis:
  867. self.push(self.inventory, region, dest)
  868. if self.nested_groups:
  869. self.push_group(self.inventory, 'regions', region)
  870. # Inventory: Group by availability zone
  871. if self.group_by_availability_zone and not is_redis:
  872. self.push(self.inventory, cluster['PreferredAvailabilityZone'], dest)
  873. if self.nested_groups:
  874. if self.group_by_region:
  875. self.push_group(self.inventory, region, cluster['PreferredAvailabilityZone'])
  876. self.push_group(self.inventory, 'zones', cluster['PreferredAvailabilityZone'])
  877. # Inventory: Group by node type
  878. if self.group_by_instance_type and not is_redis:
  879. type_name = self.to_safe('type_' + cluster['CacheNodeType'])
  880. self.push(self.inventory, type_name, dest)
  881. if self.nested_groups:
  882. self.push_group(self.inventory, 'types', type_name)
  883. # Inventory: Group by VPC (information not available in the current
  884. # AWS API version for ElastiCache)
  885. # Inventory: Group by security group
  886. if self.group_by_security_group and not is_redis:
  887. # Check for the existence of the 'SecurityGroups' key and also if
  888. # this key has some value. When the cluster is not placed in a SG
  889. # the query can return None here and cause an error.
  890. if 'SecurityGroups' in cluster and cluster['SecurityGroups'] is not None:
  891. for security_group in cluster['SecurityGroups']:
  892. key = self.to_safe("security_group_" + security_group['SecurityGroupId'])
  893. self.push(self.inventory, key, dest)
  894. if self.nested_groups:
  895. self.push_group(self.inventory, 'security_groups', key)
  896. # Inventory: Group by engine
  897. if self.group_by_elasticache_engine and not is_redis:
  898. self.push(self.inventory, self.to_safe("elasticache_" + cluster['Engine']), dest)
  899. if self.nested_groups:
  900. self.push_group(self.inventory, 'elasticache_engines', self.to_safe(cluster['Engine']))
  901. # Inventory: Group by parameter group
  902. if self.group_by_elasticache_parameter_group:
  903. self.push(self.inventory, self.to_safe("elasticache_parameter_group_" + cluster['CacheParameterGroup']['CacheParameterGroupName']), dest)
  904. if self.nested_groups:
  905. self.push_group(self.inventory, 'elasticache_parameter_groups', self.to_safe(cluster['CacheParameterGroup']['CacheParameterGroupName']))
  906. # Inventory: Group by replication group
  907. if self.group_by_elasticache_replication_group and 'ReplicationGroupId' in cluster and cluster['ReplicationGroupId']:
  908. self.push(self.inventory, self.to_safe("elasticache_replication_group_" + cluster['ReplicationGroupId']), dest)
  909. if self.nested_groups:
  910. self.push_group(self.inventory, 'elasticache_replication_groups', self.to_safe(cluster['ReplicationGroupId']))
  911. # Global Tag: all ElastiCache clusters
  912. self.push(self.inventory, 'elasticache_clusters', cluster['CacheClusterId'])
  913. host_info = self.get_host_info_dict_from_describe_dict(cluster)
  914. self.inventory["_meta"]["hostvars"][dest] = host_info
  915. # Add the nodes
  916. for node in cluster['CacheNodes']:
  917. self.add_elasticache_node(node, cluster, region)
  918. def add_elasticache_node(self, node, cluster, region):
  919. ''' Adds an ElastiCache node to the inventory and index, as long as
  920. it is addressable '''
  921. # Only want available nodes unless all_elasticache_nodes is True
  922. if not self.all_elasticache_nodes and node['CacheNodeStatus'] != 'available':
  923. return
  924. # Select the best destination address
  925. dest = node['Endpoint']['Address']
  926. if not dest:
  927. # Skip nodes we cannot address (e.g. private VPC subnet)
  928. return
  929. node_id = self.to_safe(cluster['CacheClusterId'] + '_' + node['CacheNodeId'])
  930. # Add to index
  931. self.index[dest] = [region, node_id]
  932. # Inventory: Group by node ID (always a group of 1)
  933. if self.group_by_instance_id:
  934. self.inventory[node_id] = [dest]
  935. if self.nested_groups:
  936. self.push_group(self.inventory, 'instances', node_id)
  937. # Inventory: Group by region
  938. if self.group_by_region:
  939. self.push(self.inventory, region, dest)
  940. if self.nested_groups:
  941. self.push_group(self.inventory, 'regions', region)
  942. # Inventory: Group by availability zone
  943. if self.group_by_availability_zone:
  944. self.push(self.inventory, cluster['PreferredAvailabilityZone'], dest)
  945. if self.nested_groups:
  946. if self.group_by_region:
  947. self.push_group(self.inventory, region, cluster['PreferredAvailabilityZone'])
  948. self.push_group(self.inventory, 'zones', cluster['PreferredAvailabilityZone'])
  949. # Inventory: Group by node type
  950. if self.group_by_instance_type:
  951. type_name = self.to_safe('type_' + cluster['CacheNodeType'])
  952. self.push(self.inventory, type_name, dest)
  953. if self.nested_groups:
  954. self.push_group(self.inventory, 'types', type_name)
  955. # Inventory: Group by VPC (information not available in the current
  956. # AWS API version for ElastiCache)
  957. # Inventory: Group by security group
  958. if self.group_by_security_group:
  959. # Check for the existence of the 'SecurityGroups' key and also if
  960. # this key has some value. When the cluster is not placed in a SG
  961. # the query can return None here and cause an error.
  962. if 'SecurityGroups' in cluster and cluster['SecurityGroups'] is not None:
  963. for security_group in cluster['SecurityGroups']:
  964. key = self.to_safe("security_group_" + security_group['SecurityGroupId'])
  965. self.push(self.inventory, key, dest)
  966. if self.nested_groups:
  967. self.push_group(self.inventory, 'security_groups', key)
  968. # Inventory: Group by engine
  969. if self.group_by_elasticache_engine:
  970. self.push(self.inventory, self.to_safe("elasticache_" + cluster['Engine']), dest)
  971. if self.nested_groups:
  972. self.push_group(self.inventory, 'elasticache_engines', self.to_safe("elasticache_" + cluster['Engine']))
  973. # Inventory: Group by parameter group (done at cluster level)
  974. # Inventory: Group by replication group (done at cluster level)
  975. # Inventory: Group by ElastiCache Cluster
  976. if self.group_by_elasticache_cluster:
  977. self.push(self.inventory, self.to_safe("elasticache_cluster_" + cluster['CacheClusterId']), dest)
  978. # Global Tag: all ElastiCache nodes
  979. self.push(self.inventory, 'elasticache_nodes', dest)
  980. host_info = self.get_host_info_dict_from_describe_dict(node)
  981. if dest in self.inventory["_meta"]["hostvars"]:
  982. self.inventory["_meta"]["hostvars"][dest].update(host_info)
  983. else:
  984. self.inventory["_meta"]["hostvars"][dest] = host_info
  985. def add_elasticache_replication_group(self, replication_group, region):
  986. ''' Adds an ElastiCache replication group to the inventory and index '''
  987. # Only want available clusters unless all_elasticache_replication_groups is True
  988. if not self.all_elasticache_replication_groups and replication_group['Status'] != 'available':
  989. return
  990. # Select the best destination address (PrimaryEndpoint)
  991. dest = replication_group['NodeGroups'][0]['PrimaryEndpoint']['Address']
  992. if not dest:
  993. # Skip clusters we cannot address (e.g. private VPC subnet)
  994. return
  995. # Add to index
  996. self.index[dest] = [region, replication_group['ReplicationGroupId']]
  997. # Inventory: Group by ID (always a group of 1)
  998. if self.group_by_instance_id:
  999. self.inventory[replication_group['ReplicationGroupId']] = [dest]
  1000. if self.nested_groups:
  1001. self.push_group(self.inventory, 'instances', replication_group['ReplicationGroupId'])
  1002. # Inventory: Group by region
  1003. if self.group_by_region:
  1004. self.push(self.inventory, region, dest)
  1005. if self.nested_groups:
  1006. self.push_group(self.inventory, 'regions', region)
  1007. # Inventory: Group by availability zone (doesn't apply to replication groups)
  1008. # Inventory: Group by node type (doesn't apply to replication groups)
  1009. # Inventory: Group by VPC (information not available in the current
  1010. # AWS API version for replication groups
  1011. # Inventory: Group by security group (doesn't apply to replication groups)
  1012. # Check this value in cluster level
  1013. # Inventory: Group by engine (replication groups are always Redis)
  1014. if self.group_by_elasticache_engine:
  1015. self.push(self.inventory, 'elasticache_redis', dest)
  1016. if self.nested_groups:
  1017. self.push_group(self.inventory, 'elasticache_engines', 'redis')
  1018. # Global Tag: all ElastiCache clusters
  1019. self.push(self.inventory, 'elasticache_replication_groups', replication_group['ReplicationGroupId'])
  1020. host_info = self.get_host_info_dict_from_describe_dict(replication_group)
  1021. self.inventory["_meta"]["hostvars"][dest] = host_info
  1022. def get_route53_records(self):
  1023. ''' Get and store the map of resource records to domain names that
  1024. point to them. '''
  1025. r53_conn = route53.Route53Connection()
  1026. all_zones = r53_conn.get_zones()
  1027. route53_zones = [ zone for zone in all_zones if zone.name[:-1]
  1028. not in self.route53_excluded_zones ]
  1029. self.route53_records = {}
  1030. for zone in route53_zones:
  1031. rrsets = r53_conn.get_all_rrsets(zone.id)
  1032. for record_set in rrsets:
  1033. record_name = record_set.name
  1034. if record_name.endswith('.'):
  1035. record_name = record_name[:-1]
  1036. for resource in record_set.resource_records:
  1037. self.route53_records.setdefault(resource, set())
  1038. self.route53_records[resource].add(record_name)
  1039. def get_instance_route53_names(self, instance):
  1040. ''' Check if an instance is referenced in the records we have from
  1041. Route53. If it is, return the list of domain names pointing to said
  1042. instance. If nothing points to it, return an empty list. '''
  1043. instance_attributes = [ 'public_dns_name', 'private_dns_name',
  1044. 'ip_address', 'private_ip_address' ]
  1045. name_list = set()
  1046. for attrib in instance_attributes:
  1047. try:
  1048. value = getattr(instance, attrib)
  1049. except AttributeError:
  1050. continue
  1051. if value in self.route53_records:
  1052. name_list.update(self.route53_records[value])
  1053. return list(name_list)
  1054. def get_host_info_dict_from_instance(self, instance):
  1055. instance_vars = {}
  1056. for key in vars(instance):
  1057. value = getattr(instance, key)
  1058. key = self.to_safe('ec2_' + key)
  1059. # Handle complex types
  1060. # state/previous_state changed to properties in boto in https://github.com/boto/boto/commit/a23c379837f698212252720d2af8dec0325c9518
  1061. if key == 'ec2__state':
  1062. instance_vars['ec2_state'] = instance.state or ''
  1063. instance_vars['ec2_state_code'] = instance.state_code
  1064. elif key == 'ec2__previous_state':
  1065. instance_vars['ec2_previous_state'] = instance.previous_state or ''
  1066. instance_vars['ec2_previous_state_code'] = instance.previous_state_code
  1067. elif type(value) in [int, bool]:
  1068. instance_vars[key] = value
  1069. elif isinstance(value, six.string_types):
  1070. instance_vars[key] = value.strip()
  1071. elif type(value) == type(None):
  1072. instance_vars[key] = ''
  1073. elif key == 'ec2_region':
  1074. instance_vars[key] = value.name
  1075. elif key == 'ec2__placement':
  1076. instance_vars['ec2_placement'] = value.zone
  1077. elif key == 'ec2_tags':
  1078. for k, v in value.items():
  1079. if self.expand_csv_tags and ',' in v:
  1080. v = list(map(lambda x: x.strip(), v.split(',')))
  1081. key = self.to_safe('ec2_tag_' + k)
  1082. instance_vars[key] = v
  1083. elif key == 'ec2_groups':
  1084. group_ids = []
  1085. group_names = []
  1086. for group in value:
  1087. group_ids.append(group.id)
  1088. group_names.append(group.name)
  1089. instance_vars["ec2_security_group_ids"] = ','.join([str(i) for i in group_ids])
  1090. instance_vars["ec2_security_group_names"] = ','.join([str(i) for i in group_names])
  1091. elif key == 'ec2_block_device_mapping':
  1092. instance_vars["ec2_block_devices"] = {}
  1093. for k, v in value.items():
  1094. instance_vars["ec2_block_devices"][ os.path.basename(k) ] = v.volume_id
  1095. else:
  1096. pass
  1097. # TODO Product codes if someone finds them useful
  1098. #print key
  1099. #print type(value)
  1100. #print value
  1101. return instance_vars
  1102. def get_host_info_dict_from_describe_dict(self, describe_dict):
  1103. ''' Parses the dictionary returned by the API call into a flat list
  1104. of parameters. This method should be used only when 'describe' is
  1105. used directly because Boto doesn't provide specific classes. '''
  1106. # I really don't agree with prefixing everything with 'ec2'
  1107. # because EC2, RDS and ElastiCache are different services.
  1108. # I'm just following the pattern used until now to not break any
  1109. # compatibility.
  1110. host_info = {}
  1111. for key in describe_dict:
  1112. value = describe_dict[key]
  1113. key = self.to_safe('ec2_' + self.uncammelize(key))
  1114. # Handle complex types
  1115. # Target: Memcached Cache Clusters
  1116. if key == 'ec2_configuration_endpoint' and value:
  1117. host_info['ec2_configuration_endpoint_address'] = value['Address']
  1118. host_info['ec2_configuration_endpoint_port'] = value['Port']
  1119. # Target: Cache Nodes and Redis Cache Clusters (single node)
  1120. if key == 'ec2_endpoint' and value:
  1121. host_info['ec2_endpoint_address'] = value['Address']
  1122. host_info['ec2_endpoint_port'] = value['Port']
  1123. # Target: Redis Replication Groups
  1124. if key == 'ec2_node_groups' and value:
  1125. host_info['ec2_endpoint_address'] = value[0]['PrimaryEndpoint']['Address']
  1126. host_info['ec2_endpoint_port'] = value[0]['PrimaryEndpoint']['Port']
  1127. replica_count = 0
  1128. for node in value[0]['NodeGroupMembers']:
  1129. if node['CurrentRole'] == 'primary':
  1130. host_info['ec2_primary_cluster_address'] = node['ReadEndpoint']['Address']
  1131. host_info['ec2_primary_cluster_port'] = node['ReadEndpoint']['Port']
  1132. host_info['ec2_primary_cluster_id'] = node['CacheClusterId']
  1133. elif node['CurrentRole'] == 'replica':
  1134. host_info['ec2_replica_cluster_address_'+ str(replica_count)] = node['ReadEndpoint']['Address']
  1135. host_info['ec2_replica_cluster_port_'+ str(replica_count)] = node['ReadEndpoint']['Port']
  1136. host_info['ec2_replica_cluster_id_'+ str(replica_count)] = node['CacheClusterId']
  1137. replica_count += 1
  1138. # Target: Redis Replication Groups
  1139. if key == 'ec2_member_clusters' and value:
  1140. host_info['ec2_member_clusters'] = ','.join([str(i) for i in value])
  1141. # Target: All Cache Clusters
  1142. elif key == 'ec2_cache_parameter_group':
  1143. host_info["ec2_cache_node_ids_to_reboot"] = ','.join([str(i) for i in value['CacheNodeIdsToReboot']])
  1144. host_info['ec2_cache_parameter_group_name'] = value['CacheParameterGroupName']
  1145. host_info['ec2_cache_parameter_apply_status'] = value['ParameterApplyStatus']
  1146. # Target: Almost everything
  1147. elif key == 'ec2_security_groups':
  1148. # Skip if SecurityGroups is None
  1149. # (it is possible to have the key defined but no value in it).
  1150. if value is not None:
  1151. sg_ids = []
  1152. for sg in value:
  1153. sg_ids.append(sg['SecurityGroupId'])
  1154. host_info["ec2_security_group_ids"] = ','.join([str(i) for i in sg_ids])
  1155. # Target: Everything
  1156. # Preserve booleans and integers
  1157. elif type(value) in [int, bool]:
  1158. host_info[key] = value
  1159. # Target: Everything
  1160. # Sanitize string values
  1161. elif isinstance(value, six.string_types):
  1162. host_info[key] = value.strip()
  1163. # Target: Everything
  1164. # Replace None by an empty string
  1165. elif type(value) == type(None):
  1166. host_info[key] = ''
  1167. else:
  1168. # Remove non-processed complex types
  1169. pass
  1170. return host_info
  1171. def get_host_info(self):
  1172. ''' Get variables about a specific host '''
  1173. if len(self.index) == 0:
  1174. # Need to load index from cache
  1175. self.load_index_from_cache()
  1176. if not self.args.host in self.index:
  1177. # try updating the cache
  1178. self.do_api_calls_update_cache()
  1179. if not self.args.host in self.index:
  1180. # host might not exist anymore
  1181. return self.json_format_dict({}, True)
  1182. (region, instance_id) = self.index[self.args.host]
  1183. instance = self.get_instance(region, instance_id)
  1184. return self.json_format_dict(self.get_host_info_dict_from_instance(instance), True)
  1185. def push(self, my_dict, key, element):
  1186. ''' Push an element onto an array that may not have been defined in
  1187. the dict '''
  1188. group_info = my_dict.setdefault(key, [])
  1189. if isinstance(group_info, dict):
  1190. host_list = group_info.setdefault('hosts', [])
  1191. host_list.append(element)
  1192. else:
  1193. group_info.append(element)
  1194. def push_group(self, my_dict, key, element):
  1195. ''' Push a group as a child of another group. '''
  1196. parent_group = my_dict.setdefault(key, {})
  1197. if not isinstance(parent_group, dict):
  1198. parent_group = my_dict[key] = {'hosts': parent_group}
  1199. child_groups = parent_group.setdefault('children', [])
  1200. if element not in child_groups:
  1201. child_groups.append(element)
  1202. def get_inventory_from_cache(self):
  1203. ''' Reads the inventory from the cache file and returns it as a JSON
  1204. object '''
  1205. cache = open(self.cache_path_cache, 'r')
  1206. json_inventory = cache.read()
  1207. return json_inventory
  1208. def load_index_from_cache(self):
  1209. ''' Reads the index from the cache file sets self.index '''
  1210. cache = open(self.cache_path_index, 'r')
  1211. json_index = cache.read()
  1212. self.index = json.loads(json_index)
  1213. def write_to_cache(self, data, filename):
  1214. ''' Writes data in JSON format to a file '''
  1215. json_data = self.json_format_dict(data, True)
  1216. cache = open(filename, 'w')
  1217. cache.write(json_data)
  1218. cache.close()
  1219. def uncammelize(self, key):
  1220. temp = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', key)
  1221. return re.sub('([a-z0-9])([A-Z])', r'\1_\2', temp).lower()
  1222. def to_safe(self, word):
  1223. ''' Converts 'bad' characters in a string to underscores so they can be used as Ansible groups '''
  1224. regex = "[^A-Za-z0-9\_"
  1225. if not self.replace_dash_in_groups:
  1226. regex += "\-"
  1227. return re.sub(regex + "]", "_", word)
  1228. def json_format_dict(self, data, pretty=False):
  1229. ''' Converts a dict to a JSON object and dumps it as a formatted
  1230. string '''
  1231. if pretty:
  1232. return json.dumps(data, sort_keys=True, indent=2)
  1233. else:
  1234. return json.dumps(data)
  1235. # Run the script
  1236. Ec2Inventory()