11
11
import json
12
12
import re
13
13
import sys
14
- import os
15
14
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
21
17
22
18
23
19
class Ec2Inventory (object ):
24
- def _empty_inventory (self ):
20
+ @staticmethod
21
+ def _empty_inventory ():
25
22
return {"_meta" : {"hostvars" : {}}}
26
23
27
- def __init__ (self , boto_profile , regions , filters = {} , bastion_filters = {} ):
24
+ def __init__ (self , boto_profile , regions , filters = None , bastion_filters = None ):
28
25
29
- self .filters = filters
26
+ self .filters = filters or []
30
27
self .regions = regions .split (',' )
31
28
self .boto_profile = boto_profile
32
- self .bastion_filters = bastion_filters
29
+ self .bastion_filters = bastion_filters or []
33
30
self .group_callbacks = []
31
+ self .boto3_session = self .create_boto3_session (boto_profile )
34
32
35
33
# Inventory grouped by instance IDs, tags, security groups, regions,
36
34
# and availability zones
@@ -39,6 +37,25 @@ def __init__(self, boto_profile, regions, filters={}, bastion_filters={}):
39
37
# Index of hostname (address) to instance ID
40
38
self .index = {}
41
39
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
+
42
59
def get_as_json (self ):
43
60
self .do_api_calls_update_cache ()
44
61
return self .json_format_dict (self .inventory , True )
@@ -55,20 +72,20 @@ def exclude(self, *args):
55
72
def group (self , * args ):
56
73
self .group_callbacks .extend (args )
57
74
58
- def find_bastion_box (self , conn ):
75
+ def find_bastion_box (self , ec2_client ):
59
76
"""
60
77
Find ips for the bastion box
61
78
"""
62
79
63
- if not self .bastion_filters . values () :
80
+ if not self .bastion_filters :
64
81
return
65
82
66
- self .bastion_filters [ ' instance-state-name'] = 'running'
83
+ self .bastion_filters . append ({ 'Name' : ' instance-state-name', 'Values' : [ 'running' ]})
67
84
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' ]
72
89
73
90
def do_api_calls_update_cache (self ):
74
91
""" Do API calls to each region, and save data in cache files """
@@ -80,92 +97,67 @@ def get_instances_by_region(self, region):
80
97
"""Makes an AWS EC2 API call to the list of instances in a particular
81
98
region
82
99
"""
100
+ ec2_client = self .boto3_session .client ('ec2' , region_name = region )
83
101
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' ]
101
103
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' ])
117
108
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' ])
121
114
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 )
124
117
125
118
def get_instance (self , region , instance_id ):
126
119
""" 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 )
129
121
# connect_to_region will fail "silently" by returning None if the
130
122
# region name is wrong or not supported
131
- if conn is None :
123
+ if ec2_client is None :
132
124
sys .exit (
133
125
"region name: %s likely not supported, or AWS is down. "
134
126
"connection to region failed." % region
135
127
)
136
128
137
- reservations = conn . get_all_instances ( [instance_id ])
129
+ reservations = ec2_client . describe_instances ( InstanceIds = [instance_id ])[ 'Reservations' ]
138
130
for reservation in reservations :
139
- for instance in reservation . instances :
131
+ for instance in reservation [ 'Instances' ] :
140
132
return instance
141
133
142
134
def add_instance (self , bastion_ip , instance , region ):
143
135
"""
144
- :type instance: boto.ec2.instance.Instance
136
+ :type instance: dict
145
137
"""
146
138
147
139
# Only want running instances unless all_instances is True
148
- if instance . state != 'running' :
140
+ if instance [ 'State' ][ 'Name' ] != 'running' :
149
141
return
150
142
151
143
# 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' ) )
153
145
if not dest :
154
146
return
155
147
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' )
160
152
else :
161
- ansible_ssh_host = instance .private_ip_address
153
+ ansible_ssh_host = instance .get ( 'PrivateIpAddress' )
162
154
163
155
# Add to index and append the instance id afterwards if it's already
164
156
# there
165
157
if dest in self .index :
166
- dest = dest + "-" + instance . id .replace ("i-" , "" )
158
+ dest = dest + "-" + instance [ 'InstanceId' ] .replace ("i-" , "" )
167
159
168
- self .index [dest ] = [region , instance . id ]
160
+ self .index [dest ] = [region , instance [ 'InstanceId' ] ]
169
161
170
162
# group with dynamic groups
171
163
for grouping in set (self .group_callbacks ):
@@ -175,9 +167,9 @@ def add_instance(self, bastion_ip, instance, region):
175
167
self .push (self .inventory , group , dest )
176
168
177
169
# 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 )
181
173
182
174
# Inventory: Group by region
183
175
self .push (self .inventory , region , dest )
@@ -186,56 +178,39 @@ def add_instance(self, bastion_ip, instance, region):
186
178
self .push (self .inventory , ansible_ssh_host , dest )
187
179
188
180
# Inventory: Group by availability zone
189
- self .push (self .inventory , instance . placement , dest )
181
+ self .push (self .inventory , instance [ 'Placement' ][ 'AvailabilityZone' ] , dest )
190
182
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 )
193
184
self .inventory ["_meta" ]["hostvars" ][dest ]['ansible_ssh_host' ] = ansible_ssh_host
194
185
195
186
def get_host_info_dict_from_instance (self , instance ):
196
187
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 ()
214
198
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 ]
230
209
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' , '' )
239
214
return instance_vars
240
215
241
216
def get_host_info (self ):
0 commit comments