ec2.py 54 KB

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