You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1595 lines
66 KiB

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