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