1111import json
1212import re
1313import sys
14- import os
1514
16- import boto
17- from boto import ec2
18- from boto .pyami .config import Config
19-
20- from six import iteritems , string_types , integer_types
15+ import boto3
16+ from botocore .exceptions import NoRegionError , NoCredentialsError , PartialCredentialsError
2117
2218
2319class Ec2Inventory (object ):
24- def _empty_inventory (self ):
20+ @staticmethod
21+ def _empty_inventory ():
2522 return {"_meta" : {"hostvars" : {}}}
2623
27- def __init__ (self , boto_profile , regions , filters = {} , bastion_filters = {} ):
24+ def __init__ (self , boto_profile , regions , filters = None , bastion_filters = None ):
2825
29- self .filters = filters
26+ self .filters = filters or []
3027 self .regions = regions .split (',' )
3128 self .boto_profile = boto_profile
32- self .bastion_filters = bastion_filters
29+ self .bastion_filters = bastion_filters or []
3330 self .group_callbacks = []
31+ self .boto3_session = self .create_boto3_session (boto_profile )
3432
3533 # Inventory grouped by instance IDs, tags, security groups, regions,
3634 # and availability zones
@@ -39,6 +37,25 @@ def __init__(self, boto_profile, regions, filters={}, bastion_filters={}):
3937 # Index of hostname (address) to instance ID
4038 self .index = {}
4139
40+ def create_boto3_session (self , profile_name ):
41+ try :
42+ # Use the profile to create a session
43+ session = boto3 .Session (profile_name = profile_name )
44+
45+ # Verify region
46+ if not self .regions :
47+ if not session .region_name :
48+ raise NoRegionError
49+ self .regions = [session .region_name ]
50+
51+ except NoRegionError :
52+ sys .exit (f"Region not specified and could not be determined for profile: { profile_name } " )
53+ except (NoCredentialsError , PartialCredentialsError ):
54+ sys .exit (f"Credentials not found or incomplete for profile: { profile_name } " )
55+ except Exception as e :
56+ sys .exit (f"An error occurred: { str (e )} " )
57+ return session
58+
4259 def get_as_json (self ):
4360 self .do_api_calls_update_cache ()
4461 return self .json_format_dict (self .inventory , True )
@@ -55,20 +72,20 @@ def exclude(self, *args):
5572 def group (self , * args ):
5673 self .group_callbacks .extend (args )
5774
58- def find_bastion_box (self , conn ):
75+ def find_bastion_box (self , ec2_client ):
5976 """
6077 Find ips for the bastion box
6178 """
6279
63- if not self .bastion_filters . values () :
80+ if not self .bastion_filters :
6481 return
6582
66- self .bastion_filters [ ' instance-state-name'] = 'running'
83+ self .bastion_filters . append ({ 'Name' : ' instance-state-name', 'Values' : [ 'running' ]})
6784
68- for reservation in conn . get_all_instances (
69- filters = self . bastion_filters ) :
70- for instance in reservation . instances :
71- return instance . ip_address
85+ reservations = ec2_client . describe_instances ( Filters = self . bastion_filters )[ 'Reservations' ]
86+ for reservation in reservations :
87+ for instance in reservation [ 'Instances' ] :
88+ return instance [ 'PublicIpAddress' ]
7289
7390 def do_api_calls_update_cache (self ):
7491 """ Do API calls to each region, and save data in cache files """
@@ -80,92 +97,67 @@ def get_instances_by_region(self, region):
8097 """Makes an AWS EC2 API call to the list of instances in a particular
8198 region
8299 """
100+ ec2_client = self .boto3_session .client ('ec2' , region_name = region )
83101
84- try :
85- cfg = Config ()
86- cfg .load_credential_file (os .path .expanduser ("~/.aws/credentials" ))
87- cfg .load_credential_file (os .path .expanduser ("~/.aws/config" ))
88- session_token = cfg .get (self .boto_profile , "aws_session_token" )
89-
90- conn = ec2 .connect_to_region (
91- region ,
92- security_token = session_token ,
93- profile_name = self .boto_profile )
94-
95- # connect_to_region will fail "silently" by returning None if the
96- # region name is wrong or not supported
97- if conn is None :
98- sys .exit (
99- "region name: {} likely not supported, or AWS is down. "
100- "connection to region failed." .format (region ))
102+ reservations = ec2_client .describe_instances (Filters = self .filters )['Reservations' ]
101103
102- reservations = conn .get_all_instances (filters = self .filters )
103-
104- bastion_ip = self .find_bastion_box (conn )
105-
106- instances = []
107- for reservation in reservations :
108- instances .extend (reservation .instances )
109-
110- # sort the instance based on name and index, in this order
111- def sort_key (instance ):
112- name = instance .tags .get ('Name' , '' )
113- return "{}-{}" .format (name , instance .id )
114-
115- for instance in sorted (instances , key = sort_key ):
116- self .add_instance (bastion_ip , instance , region )
104+ bastion_ip = self .find_bastion_box (ec2_client )
105+ instances = []
106+ for reservation in reservations :
107+ instances .extend (reservation ['Instances' ])
117108
118- except boto .provider .ProfileNotFoundError as e :
119- raise Exception (
120- "{}, configure it with 'aws configure --profile {}'" .format (e .message , self .boto_profile ))
109+ # sort the instance based on name and index, in this order
110+ def sort_key (instance ):
111+ name = next ((tag ['Value' ] for tag in instance .get ('Tags' , [])
112+ if tag ['Key' ] == 'Name' ), '' )
113+ return "{}-{}" .format (name , instance ['InstanceId' ])
121114
122- except boto . exception . BotoServerError as e :
123- sys . exit ( e )
115+ for instance in sorted ( instances , key = sort_key ) :
116+ self . add_instance ( bastion_ip , instance , region )
124117
125118 def get_instance (self , region , instance_id ):
126119 """ Gets details about a specific instance """
127- conn = ec2 .connect_to_region (region )
128-
120+ ec2_client = self .boto3_session .client ('ec2' , region_name = region )
129121 # connect_to_region will fail "silently" by returning None if the
130122 # region name is wrong or not supported
131- if conn is None :
123+ if ec2_client is None :
132124 sys .exit (
133125 "region name: %s likely not supported, or AWS is down. "
134126 "connection to region failed." % region
135127 )
136128
137- reservations = conn . get_all_instances ( [instance_id ])
129+ reservations = ec2_client . describe_instances ( InstanceIds = [instance_id ])[ 'Reservations' ]
138130 for reservation in reservations :
139- for instance in reservation . instances :
131+ for instance in reservation [ 'Instances' ] :
140132 return instance
141133
142134 def add_instance (self , bastion_ip , instance , region ):
143135 """
144- :type instance: boto.ec2.instance.Instance
136+ :type instance: dict
145137 """
146138
147139 # Only want running instances unless all_instances is True
148- if instance . state != 'running' :
140+ if instance [ 'State' ][ 'Name' ] != 'running' :
149141 return
150142
151143 # Use the instance name instead of the public ip
152- dest = instance .tags . get ('Name' , instance .ip_address )
144+ dest = next (( tag [ 'Value' ] for tag in instance .get ('Tags' , []) if tag [ 'Key' ] == ' Name') , instance .get ( 'PublicIpAddress' ) )
153145 if not dest :
154146 return
155147
156- if bastion_ip and bastion_ip != instance .ip_address :
157- ansible_ssh_host = bastion_ip + "--" + instance .private_ip_address
158- elif instance .ip_address :
159- ansible_ssh_host = instance .ip_address
148+ if bastion_ip and bastion_ip != instance .get ( 'PublicIpAddress' ) :
149+ ansible_ssh_host = bastion_ip + "--" + instance .get ( 'PrivateIpAddress' )
150+ elif instance .get ( 'PublicIpAddress' ) :
151+ ansible_ssh_host = instance .get ( 'PublicIpAddress' )
160152 else :
161- ansible_ssh_host = instance .private_ip_address
153+ ansible_ssh_host = instance .get ( 'PrivateIpAddress' )
162154
163155 # Add to index and append the instance id afterwards if it's already
164156 # there
165157 if dest in self .index :
166- dest = dest + "-" + instance . id .replace ("i-" , "" )
158+ dest = dest + "-" + instance [ 'InstanceId' ] .replace ("i-" , "" )
167159
168- self .index [dest ] = [region , instance . id ]
160+ self .index [dest ] = [region , instance [ 'InstanceId' ] ]
169161
170162 # group with dynamic groups
171163 for grouping in set (self .group_callbacks ):
@@ -175,9 +167,9 @@ def add_instance(self, bastion_ip, instance, region):
175167 self .push (self .inventory , group , dest )
176168
177169 # Group by all tags
178- for tag in instance .tags . values ( ):
179- if tag :
180- self .push (self .inventory , tag , dest )
170+ for tag in instance .get ( 'Tags' , [] ):
171+ if tag [ 'Value' ] :
172+ self .push (self .inventory , tag [ 'Value' ] , dest )
181173
182174 # Inventory: Group by region
183175 self .push (self .inventory , region , dest )
@@ -186,56 +178,39 @@ def add_instance(self, bastion_ip, instance, region):
186178 self .push (self .inventory , ansible_ssh_host , dest )
187179
188180 # Inventory: Group by availability zone
189- self .push (self .inventory , instance . placement , dest )
181+ self .push (self .inventory , instance [ 'Placement' ][ 'AvailabilityZone' ] , dest )
190182
191- self .inventory ["_meta" ]["hostvars" ][dest ] = self .get_host_info_dict_from_instance (
192- instance )
183+ self .inventory ["_meta" ]["hostvars" ][dest ] = self .get_host_info_dict_from_instance (instance )
193184 self .inventory ["_meta" ]["hostvars" ][dest ]['ansible_ssh_host' ] = ansible_ssh_host
194185
195186 def get_host_info_dict_from_instance (self , instance ):
196187 instance_vars = {}
197- for key in vars (instance ):
198- value = getattr (instance , key )
199- key = self .to_safe ('ec2_' + key )
200-
201- # Handle complex types
202- # state/previous_state changed to properties in boto in
203- # https://github.com/boto/boto/commit/a23c379837f698212252720d2af8dec0325c9518
204- if key == 'ec2__state' :
205- instance_vars ['ec2_state' ] = instance .state or ''
206- instance_vars ['ec2_state_code' ] = instance .state_code
207- elif key == 'ec2__previous_state' :
208- instance_vars ['ec2_previous_state' ] = instance .previous_state or ''
209- instance_vars ['ec2_previous_state_code' ] = instance .previous_state_code
210- elif type (value ) in integer_types or isinstance (value , bool ):
211- instance_vars [key ] = value
212- elif type (value ) in string_types :
213- instance_vars [key ] = value .strip ()
188+ for key , value in instance .items ():
189+ safe_key = self .to_safe ('ec2_' + key )
190+
191+ if key == 'State' :
192+ instance_vars ['ec2_state' ] = value ['Name' ]
193+ instance_vars ['ec2_state_code' ] = value ['Code' ]
194+ elif isinstance (value , (int , bool )):
195+ instance_vars [safe_key ] = value
196+ elif isinstance (value , str ):
197+ instance_vars [safe_key ] = value .strip ()
214198 elif value is None :
215- instance_vars [key ] = ''
216- elif key == 'ec2_region' :
217- instance_vars [key ] = value .name
218- elif key == 'ec2__placement' :
219- instance_vars ['ec2_placement' ] = value .zone
220- elif key == 'ec2_tags' :
221- for k , v in iteritems (value ):
222- key = self .to_safe ('ec2_tag_' + k )
223- instance_vars [key ] = v
224- elif key == 'ec2_groups' :
225- group_ids = []
226- group_names = []
227- for group in value :
228- group_ids .append (group .id )
229- group_names .append (group .name )
199+ instance_vars [safe_key ] = ''
200+ elif key == 'Placement' :
201+ instance_vars ['ec2_placement' ] = value ['AvailabilityZone' ]
202+ elif key == 'Tags' :
203+ for tag in value :
204+ tag_key = self .to_safe ('ec2_tag_' + tag ['Key' ])
205+ instance_vars [tag_key ] = tag ['Value' ]
206+ elif key == 'SecurityGroups' :
207+ group_ids = [group ['GroupId' ] for group in value ]
208+ group_names = [group ['GroupName' ] for group in value ]
230209 instance_vars ["ec2_security_group_ids" ] = ',' .join (group_ids )
231- instance_vars ["ec2_security_group_names" ] = ',' .join (
232- group_names )
233- # add non ec2 prefix private ip address that are being used in cross provider command
234- # e.g ssh, sync
235- instance_vars ['private_ip' ] = instance_vars .get (
236- 'ec2_private_ip_address' , '' )
237- instance_vars ['private_ip_address' ] = instance_vars .get (
238- 'ec2_private_ip_address' , '' )
210+ instance_vars ["ec2_security_group_names" ] = ',' .join (group_names )
211+
212+ instance_vars ['private_ip' ] = instance .get ('PrivateIpAddress' , '' )
213+ instance_vars ['private_ip_address' ] = instance .get ('PrivateIpAddress' , '' )
239214 return instance_vars
240215
241216 def get_host_info (self ):
0 commit comments