Browse Source

Merge pull request #95 from a13m/destination_format

Add flexible destination format string to ec2.py
Thomas Wiest 10 năm trước cách đây
mục cha
commit
fbf0302567
2 tập tin đã thay đổi với 251 bổ sung62 xóa
  1. 7 0
      inventory/aws/ec2.ini
  2. 244 62
      inventory/aws/ec2.py

+ 7 - 0
inventory/aws/ec2.ini

@@ -53,3 +53,10 @@ cache_path = ~/.ansible/tmp
 # seconds, a new API call will be made, and the cache file will be updated.
 # seconds, a new API call will be made, and the cache file will be updated.
 # To disable the cache, set this value to 0
 # To disable the cache, set this value to 0
 cache_max_age = 300
 cache_max_age = 300
+
+# These two settings allow flexible ansible host naming based on a format
+# string and a comma-separated list of ec2 tags.  The tags used must be
+# present for all instances, or the code will fail.  This overrides both
+# destination_variable and vpc_destination_variable.
+# destination_format = {0}.{1}.rhcloud.com
+# destination_format_tags = Name,environment

+ 244 - 62
inventory/aws/ec2.py

@@ -123,6 +123,7 @@ from boto import ec2
 from boto import rds
 from boto import rds
 from boto import route53
 from boto import route53
 import ConfigParser
 import ConfigParser
+from collections import defaultdict
 
 
 try:
 try:
     import json
     import json
@@ -214,6 +215,8 @@ class Ec2Inventory(object):
         # Destination addresses
         # Destination addresses
         self.destination_variable = config.get('ec2', 'destination_variable')
         self.destination_variable = config.get('ec2', 'destination_variable')
         self.vpc_destination_variable = config.get('ec2', 'vpc_destination_variable')
         self.vpc_destination_variable = config.get('ec2', 'vpc_destination_variable')
+        self.destination_format = config.get('ec2', 'destination_format')
+        self.destination_format_tags = config.get('ec2', 'destination_format_tags', '').split(',')
 
 
         # Route53
         # Route53
         self.route53_enabled = config.getboolean('ec2', 'route53')
         self.route53_enabled = config.getboolean('ec2', 'route53')
@@ -222,6 +225,21 @@ class Ec2Inventory(object):
             self.route53_excluded_zones.extend(
             self.route53_excluded_zones.extend(
                 config.get('ec2', 'route53_excluded_zones', '').split(','))
                 config.get('ec2', 'route53_excluded_zones', '').split(','))
 
 
+        # Include RDS instances?
+        self.rds_enabled = True
+        if config.has_option('ec2', 'rds'):
+            self.rds_enabled = config.getboolean('ec2', 'rds')
+
+        # Return all EC2 and RDS instances (if RDS is enabled)
+        if config.has_option('ec2', 'all_instances'):
+            self.all_instances = config.getboolean('ec2', 'all_instances')
+        else:
+            self.all_instances = False
+        if config.has_option('ec2', 'all_rds_instances') and self.rds_enabled:
+            self.all_rds_instances = config.getboolean('ec2', 'all_rds_instances')
+        else:
+            self.all_rds_instances = False
+
         # Cache related
         # Cache related
         cache_dir = os.path.expanduser(config.get('ec2', 'cache_path'))
         cache_dir = os.path.expanduser(config.get('ec2', 'cache_path'))
         if not os.path.exists(cache_dir):
         if not os.path.exists(cache_dir):
@@ -230,8 +248,66 @@ class Ec2Inventory(object):
         self.cache_path_cache = cache_dir + "/ansible-ec2.cache"
         self.cache_path_cache = cache_dir + "/ansible-ec2.cache"
         self.cache_path_index = cache_dir + "/ansible-ec2.index"
         self.cache_path_index = cache_dir + "/ansible-ec2.index"
         self.cache_max_age = config.getint('ec2', 'cache_max_age')
         self.cache_max_age = config.getint('ec2', 'cache_max_age')
-        
 
 
+        # Configure nested groups instead of flat namespace.
+        if config.has_option('ec2', 'nested_groups'):
+            self.nested_groups = config.getboolean('ec2', 'nested_groups')
+        else:
+            self.nested_groups = False
+
+        # Configure which groups should be created.
+        group_by_options = [
+            'group_by_instance_id',
+            'group_by_region',
+            'group_by_availability_zone',
+            'group_by_ami_id',
+            'group_by_instance_type',
+            'group_by_key_pair',
+            'group_by_vpc_id',
+            'group_by_security_group',
+            'group_by_tag_keys',
+            'group_by_tag_none',
+            'group_by_route53_names',
+            'group_by_rds_engine',
+            'group_by_rds_parameter_group',
+        ]
+        for option in group_by_options:
+            if config.has_option('ec2', option):
+                setattr(self, option, config.getboolean('ec2', option))
+            else:
+                setattr(self, option, True)
+
+        # Do we need to just include hosts that match a pattern?
+        try:
+            pattern_include = config.get('ec2', 'pattern_include')
+            if pattern_include and len(pattern_include) > 0:
+                self.pattern_include = re.compile(pattern_include)
+            else:
+                self.pattern_include = None
+        except ConfigParser.NoOptionError, e:
+            self.pattern_include = None
+
+        # Do we need to exclude hosts that match a pattern?
+        try:
+            pattern_exclude = config.get('ec2', 'pattern_exclude');
+            if pattern_exclude and len(pattern_exclude) > 0:
+                self.pattern_exclude = re.compile(pattern_exclude)
+            else:
+                self.pattern_exclude = None
+        except ConfigParser.NoOptionError, e:
+            self.pattern_exclude = None
+
+        # Instance filters (see boto and EC2 API docs). Ignore invalid filters.
+        self.ec2_instance_filters = defaultdict(list)
+        if config.has_option('ec2', 'instance_filters'):
+            for instance_filter in config.get('ec2', 'instance_filters', '').split(','):
+                instance_filter = instance_filter.strip()
+                if not instance_filter or '=' not in instance_filter:
+                    continue
+                filter_key, filter_value = [x.strip() for x in instance_filter.split('=', 1)]
+                if not filter_key:
+                    continue
+                self.ec2_instance_filters[filter_key].append(filter_value)
 
 
     def parse_cli_args(self):
     def parse_cli_args(self):
         ''' Command line argument processing '''
         ''' Command line argument processing '''
@@ -254,7 +330,8 @@ class Ec2Inventory(object):
 
 
         for region in self.regions:
         for region in self.regions:
             self.get_instances_by_region(region)
             self.get_instances_by_region(region)
-            self.get_rds_instances_by_region(region)
+            if self.rds_enabled:
+                self.get_rds_instances_by_region(region)
 
 
         self.write_to_cache(self.inventory, self.cache_path_cache)
         self.write_to_cache(self.inventory, self.cache_path_cache)
         self.write_to_cache(self.index, self.cache_path_index)
         self.write_to_cache(self.index, self.cache_path_index)
@@ -275,12 +352,18 @@ class Ec2Inventory(object):
             if conn is None:
             if conn is None:
                 print("region name: %s likely not supported, or AWS is down.  connection to region failed." % region)
                 print("region name: %s likely not supported, or AWS is down.  connection to region failed." % region)
                 sys.exit(1)
                 sys.exit(1)
- 
-            reservations = conn.get_all_instances()
+
+            reservations = []
+            if self.ec2_instance_filters:
+                for filter_key, filter_values in self.ec2_instance_filters.iteritems():
+                    reservations.extend(conn.get_all_instances(filters = { filter_key : filter_values }))
+            else:
+                reservations = conn.get_all_instances()
+
             for reservation in reservations:
             for reservation in reservations:
                 for instance in reservation.instances:
                 for instance in reservation.instances:
                     self.add_instance(instance, region)
                     self.add_instance(instance, region)
-        
+
         except boto.exception.BotoServerError, e:
         except boto.exception.BotoServerError, e:
             if  not self.eucalyptus:
             if  not self.eucalyptus:
                 print "Looks like AWS is down again:"
                 print "Looks like AWS is down again:"
@@ -288,7 +371,7 @@ class Ec2Inventory(object):
             sys.exit(1)
             sys.exit(1)
 
 
     def get_rds_instances_by_region(self, region):
     def get_rds_instances_by_region(self, region):
-	''' Makes an AWS API call to the list of RDS instances in a particular
+        ''' Makes an AWS API call to the list of RDS instances in a particular
         region '''
         region '''
 
 
         try:
         try:
@@ -321,64 +404,124 @@ class Ec2Inventory(object):
             for instance in reservation.instances:
             for instance in reservation.instances:
                 return instance
                 return instance
 
 
-
     def add_instance(self, instance, region):
     def add_instance(self, instance, region):
         ''' Adds an instance to the inventory and index, as long as it is
         ''' Adds an instance to the inventory and index, as long as it is
         addressable '''
         addressable '''
 
 
-        # Only want running instances
-        if instance.state != 'running':
+        # Only want running instances unless all_instances is True
+        if not self.all_instances and instance.state != 'running':
             return
             return
 
 
         # Select the best destination address
         # Select the best destination address
-        if instance.subnet_id:
-            dest = getattr(instance, self.vpc_destination_variable)
+        if self.destination_format and self.destination_format_tags:
+            dest = self.destination_format.format(*[ getattr(instance, 'tags').get(tag, 'nil') for tag in self.destination_format_tags ])
+        elif instance.subnet_id:
+            dest = getattr(instance, self.vpc_destination_variable, None)
+            if dest is None:
+                dest = getattr(instance, 'tags').get(self.vpc_destination_variable, None)
         else:
         else:
-            dest =  getattr(instance, self.destination_variable)
+            dest = getattr(instance, self.destination_variable, None)
+            if dest is None:
+                dest = getattr(instance, 'tags').get(self.destination_variable, None)
 
 
         if not dest:
         if not dest:
             # Skip instances we cannot address (e.g. private VPC subnet)
             # Skip instances we cannot address (e.g. private VPC subnet)
             return
             return
 
 
+        # if we only want to include hosts that match a pattern, skip those that don't
+        if self.pattern_include and not self.pattern_include.match(dest):
+            return
+
+        # if we need to exclude hosts that match a pattern, skip those
+        if self.pattern_exclude and self.pattern_exclude.match(dest):
+            return
+
         # Add to index
         # Add to index
         self.index[dest] = [region, instance.id]
         self.index[dest] = [region, instance.id]
 
 
         # Inventory: Group by instance ID (always a group of 1)
         # Inventory: Group by instance ID (always a group of 1)
-        self.inventory[instance.id] = [dest]
+        if self.group_by_instance_id:
+            self.inventory[instance.id] = [dest]
+            if self.nested_groups:
+                self.push_group(self.inventory, 'instances', instance.id)
 
 
         # Inventory: Group by region
         # Inventory: Group by region
-        self.push(self.inventory, region, dest)
+        if self.group_by_region:
+            self.push(self.inventory, region, dest)
+            if self.nested_groups:
+                self.push_group(self.inventory, 'regions', region)
 
 
         # Inventory: Group by availability zone
         # Inventory: Group by availability zone
-        self.push(self.inventory, instance.placement, dest)
+        if self.group_by_availability_zone:
+            self.push(self.inventory, instance.placement, dest)
+            if self.nested_groups:
+                if self.group_by_region:
+                    self.push_group(self.inventory, region, instance.placement)
+                self.push_group(self.inventory, 'zones', instance.placement)
+
+        # Inventory: Group by Amazon Machine Image (AMI) ID
+        if self.group_by_ami_id:
+            ami_id = self.to_safe(instance.image_id)
+            self.push(self.inventory, ami_id, dest)
+            if self.nested_groups:
+                self.push_group(self.inventory, 'images', ami_id)
 
 
         # Inventory: Group by instance type
         # Inventory: Group by instance type
-        self.push(self.inventory, self.to_safe('type_' + instance.instance_type), dest)
+        if self.group_by_instance_type:
+            type_name = self.to_safe('type_' + instance.instance_type)
+            self.push(self.inventory, type_name, dest)
+            if self.nested_groups:
+                self.push_group(self.inventory, 'types', type_name)
 
 
         # Inventory: Group by key pair
         # Inventory: Group by key pair
-        if instance.key_name:
-            self.push(self.inventory, self.to_safe('key_' + instance.key_name), dest)
-        
+        if self.group_by_key_pair and instance.key_name:
+            key_name = self.to_safe('key_' + instance.key_name)
+            self.push(self.inventory, key_name, dest)
+            if self.nested_groups:
+                self.push_group(self.inventory, 'keys', key_name)
+
+        # Inventory: Group by VPC
+        if self.group_by_vpc_id and instance.vpc_id:
+            vpc_id_name = self.to_safe('vpc_id_' + instance.vpc_id)
+            self.push(self.inventory, vpc_id_name, dest)
+            if self.nested_groups:
+                self.push_group(self.inventory, 'vpcs', vpc_id_name)
+
         # Inventory: Group by security group
         # Inventory: Group by security group
-        try:
-            for group in instance.groups:
-                key = self.to_safe("security_group_" + group.name)
-                self.push(self.inventory, key, dest)
-        except AttributeError:
-            print 'Package boto seems a bit older.'
-            print 'Please upgrade boto >= 2.3.0.'
-            sys.exit(1)
+        if self.group_by_security_group:
+            try:
+                for group in instance.groups:
+                    key = self.to_safe("security_group_" + group.name)
+                    self.push(self.inventory, key, dest)
+                    if self.nested_groups:
+                        self.push_group(self.inventory, 'security_groups', key)
+            except AttributeError:
+                print 'Package boto seems a bit older.'
+                print 'Please upgrade boto >= 2.3.0.'
+                sys.exit(1)
 
 
         # Inventory: Group by tag keys
         # Inventory: Group by tag keys
-        for k, v in instance.tags.iteritems():
-            key = self.to_safe("tag_" + k + "=" + v)
-            self.push(self.inventory, key, dest)
+        if self.group_by_tag_keys:
+            for k, v in instance.tags.iteritems():
+                key = self.to_safe("tag_" + k + "=" + v)
+                self.push(self.inventory, key, dest)
+                if self.nested_groups:
+                    self.push_group(self.inventory, 'tags', self.to_safe("tag_" + k))
+                    self.push_group(self.inventory, self.to_safe("tag_" + k), key)
 
 
         # Inventory: Group by Route53 domain names if enabled
         # Inventory: Group by Route53 domain names if enabled
-        if self.route53_enabled:
+        if self.route53_enabled and self.group_by_route53_names:
             route53_names = self.get_instance_route53_names(instance)
             route53_names = self.get_instance_route53_names(instance)
             for name in route53_names:
             for name in route53_names:
                 self.push(self.inventory, name, dest)
                 self.push(self.inventory, name, dest)
+                if self.nested_groups:
+                    self.push_group(self.inventory, 'route53', name)
+
+        # Global Tag: instances without tags
+        if self.group_by_tag_none and len(instance.tags) == 0:
+            self.push(self.inventory, 'tag_none', dest)
+            if self.nested_groups:
+                self.push_group(self.inventory, 'tags', 'tag_none')
 
 
         # Global Tag: tag all EC2 instances
         # Global Tag: tag all EC2 instances
         self.push(self.inventory, 'ec2', dest)
         self.push(self.inventory, 'ec2', dest)
@@ -390,15 +533,11 @@ class Ec2Inventory(object):
         ''' Adds an RDS instance to the inventory and index, as long as it is
         ''' Adds an RDS instance to the inventory and index, as long as it is
         addressable '''
         addressable '''
 
 
-        # Only want available instances
-        if instance.status != 'available':
+        # Only want available instances unless all_rds_instances is True
+        if not self.all_rds_instances and instance.status != 'available':
             return
             return
 
 
         # Select the best destination address
         # Select the best destination address
-        #if instance.subnet_id:
-            #dest = getattr(instance, self.vpc_destination_variable)
-        #else:
-            #dest =  getattr(instance, self.destination_variable)
         dest = instance.endpoint[0]
         dest = instance.endpoint[0]
 
 
         if not dest:
         if not dest:
@@ -409,36 +548,70 @@ class Ec2Inventory(object):
         self.index[dest] = [region, instance.id]
         self.index[dest] = [region, instance.id]
 
 
         # Inventory: Group by instance ID (always a group of 1)
         # Inventory: Group by instance ID (always a group of 1)
-        self.inventory[instance.id] = [dest]
+        if self.group_by_instance_id:
+            self.inventory[instance.id] = [dest]
+            if self.nested_groups:
+                self.push_group(self.inventory, 'instances', instance.id)
 
 
         # Inventory: Group by region
         # Inventory: Group by region
-        self.push(self.inventory, region, dest)
+        if self.group_by_region:
+            self.push(self.inventory, region, dest)
+            if self.nested_groups:
+                self.push_group(self.inventory, 'regions', region)
 
 
         # Inventory: Group by availability zone
         # Inventory: Group by availability zone
-        self.push(self.inventory, instance.availability_zone, dest)
-        
+        if self.group_by_availability_zone:
+            self.push(self.inventory, instance.availability_zone, dest)
+            if self.nested_groups:
+                if self.group_by_region:
+                    self.push_group(self.inventory, region, instance.availability_zone)
+                self.push_group(self.inventory, 'zones', instance.availability_zone)
+
         # Inventory: Group by instance type
         # Inventory: Group by instance type
-        self.push(self.inventory, self.to_safe('type_' + instance.instance_class), dest)
-        
+        if self.group_by_instance_type:
+            type_name = self.to_safe('type_' + instance.instance_class)
+            self.push(self.inventory, type_name, dest)
+            if self.nested_groups:
+                self.push_group(self.inventory, 'types', type_name)
+
+        # Inventory: Group by VPC
+        if self.group_by_vpc_id and instance.subnet_group and instance.subnet_group.vpc_id:
+            vpc_id_name = self.to_safe('vpc_id_' + instance.subnet_group.vpc_id)
+            self.push(self.inventory, vpc_id_name, dest)
+            if self.nested_groups:
+                self.push_group(self.inventory, 'vpcs', vpc_id_name)
+
         # Inventory: Group by security group
         # Inventory: Group by security group
-        try:
-            if instance.security_group:
-                key = self.to_safe("security_group_" + instance.security_group.name)
-                self.push(self.inventory, key, dest)
-        except AttributeError:
-            print 'Package boto seems a bit older.'
-            print 'Please upgrade boto >= 2.3.0.'
-            sys.exit(1)
+        if self.group_by_security_group:
+            try:
+                if instance.security_group:
+                    key = self.to_safe("security_group_" + instance.security_group.name)
+                    self.push(self.inventory, key, dest)
+                    if self.nested_groups:
+                        self.push_group(self.inventory, 'security_groups', key)
+
+            except AttributeError:
+                print 'Package boto seems a bit older.'
+                print 'Please upgrade boto >= 2.3.0.'
+                sys.exit(1)
 
 
         # Inventory: Group by engine
         # Inventory: Group by engine
-        self.push(self.inventory, self.to_safe("rds_" + instance.engine), dest)
+        if self.group_by_rds_engine:
+            self.push(self.inventory, self.to_safe("rds_" + instance.engine), dest)
+            if self.nested_groups:
+                self.push_group(self.inventory, 'rds_engines', self.to_safe("rds_" + instance.engine))
 
 
         # Inventory: Group by parameter group
         # Inventory: Group by parameter group
-        self.push(self.inventory, self.to_safe("rds_parameter_group_" + instance.parameter_group.name), dest)
+        if self.group_by_rds_parameter_group:
+            self.push(self.inventory, self.to_safe("rds_parameter_group_" + instance.parameter_group.name), dest)
+            if self.nested_groups:
+                self.push_group(self.inventory, 'rds_parameter_groups', self.to_safe("rds_parameter_group_" + instance.parameter_group.name))
 
 
         # Global Tag: all RDS instances
         # Global Tag: all RDS instances
         self.push(self.inventory, 'rds', dest)
         self.push(self.inventory, 'rds', dest)
 
 
+        self.inventory["_meta"]["hostvars"][dest] = self.get_host_info_dict_from_instance(instance)
+
 
 
     def get_route53_records(self):
     def get_route53_records(self):
         ''' Get and store the map of resource records to domain names that
         ''' Get and store the map of resource records to domain names that
@@ -522,8 +695,8 @@ class Ec2Inventory(object):
                 for group in value:
                 for group in value:
                     group_ids.append(group.id)
                     group_ids.append(group.id)
                     group_names.append(group.name)
                     group_names.append(group.name)
-                instance_vars["ec2_security_group_ids"] = ','.join(group_ids)
-                instance_vars["ec2_security_group_names"] = ','.join(group_names)
+                instance_vars["ec2_security_group_ids"] = ','.join([str(i) for i in group_ids])
+                instance_vars["ec2_security_group_names"] = ','.join([str(i) for i in group_names])
             else:
             else:
                 pass
                 pass
                 # TODO Product codes if someone finds them useful
                 # TODO Product codes if someone finds them useful
@@ -544,7 +717,7 @@ class Ec2Inventory(object):
             # try updating the cache
             # try updating the cache
             self.do_api_calls_update_cache()
             self.do_api_calls_update_cache()
             if not self.args.host in self.index:
             if not self.args.host in self.index:
-                # host migh not exist anymore
+                # host might not exist anymore
                 return self.json_format_dict({}, True)
                 return self.json_format_dict({}, True)
 
 
         (region, instance_id) = self.index[self.args.host]
         (region, instance_id) = self.index[self.args.host]
@@ -553,14 +726,23 @@ class Ec2Inventory(object):
         return self.json_format_dict(self.get_host_info_dict_from_instance(instance), True)
         return self.json_format_dict(self.get_host_info_dict_from_instance(instance), True)
 
 
     def push(self, my_dict, key, element):
     def push(self, my_dict, key, element):
-        ''' Pushed an element onto an array that may not have been defined in
+        ''' Push an element onto an array that may not have been defined in
         the dict '''
         the dict '''
-
-        if key in my_dict:
-            my_dict[key].append(element);
+        group_info = my_dict.setdefault(key, [])
+        if isinstance(group_info, dict):
+            host_list = group_info.setdefault('hosts', [])
+            host_list.append(element)
         else:
         else:
-            my_dict[key] = [element]
-
+            group_info.append(element)
+
+    def push_group(self, my_dict, key, element):
+        ''' Push a group as a child of another group. '''
+        parent_group = my_dict.setdefault(key, {})
+        if not isinstance(parent_group, dict):
+            parent_group = my_dict[key] = {'hosts': parent_group}
+        child_groups = parent_group.setdefault('children', [])
+        if element not in child_groups:
+            child_groups.append(element)
 
 
     def get_inventory_from_cache(self):
     def get_inventory_from_cache(self):
         ''' Reads the inventory from the cache file and returns it as a JSON
         ''' Reads the inventory from the cache file and returns it as a JSON