Skip to content

Commit 1f4826c

Browse files
authored
Replace boto with boto3 (#167)
1 parent 60d3aa8 commit 1f4826c

File tree

5 files changed

+113
-135
lines changed

5 files changed

+113
-135
lines changed

requirements.txt

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
simpledi==0.4.1
2-
awscli==1.29.12
3-
boto3==1.28.12
4-
boto==2.49.0
5-
ansible==8.5.0
2+
awscli==1.32.6
3+
boto3==1.34.6
4+
botocore==1.34.6
5+
urllib3==2.0.7
6+
ansible==8.7.0
67
azure-common==1.1.28
78
azure==4.0.0
89
msrestazure==0.6.4
9-
Jinja2==3.1.2
10+
Jinja2==3.1.4
1011
hashmerge
1112
python-consul
12-
hvac==1.1.1
13+
hvac==1.2.1
1314
passgen
1415
inflection==0.5.1
1516
kubernetes==26.1.0
16-
himl==0.15.0
17+
himl==0.15.2
1718
six
1819
GitPython==3.1.*

src/ops/cli/inventory.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def run(self, args, extra_args):
5353
group_names = [group.name for group in host.get_groups()]
5454
group_names = sorted(group_names)
5555
group_string = ", ".join(group_names)
56-
host_id = host.vars.get('ec2_id', '')
56+
host_id = host.vars.get('ec2_InstanceId', '')
5757
if host_id != '':
5858
name_and_id = "%s -- %s" % (stringc(host.name,
5959
'blue'), stringc(host_id, 'blue'))

src/ops/inventory/ec2inventory.py

+90-115
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,24 @@
1111
import json
1212
import re
1313
import 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

2319
class 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):

src/ops/inventory/plugin/cns.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ def cns(args):
2727
region=region,
2828
boto_profile=profile,
2929
cache=args.get('cache', 3600 * 24),
30-
filters={
31-
'tag:cluster': cns_cluster
32-
},
33-
bastion={
34-
'tag:cluster': cns_cluster,
35-
'tag:role': 'bastion'
36-
}
30+
filters=[
31+
{'Name': 'tag:cluster', 'Values': [cns_cluster]}
32+
],
33+
bastion=[
34+
{'Name': 'tag:cluster', 'Values': [cns_cluster]},
35+
{'Name': 'tag:role', 'Values': ['bastion']}
36+
]
3737
))
3838

3939
merge_inventories(result, json.loads(jsn))

src/ops/inventory/plugin/ec2.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@
1212

1313

1414
def ec2(args):
15-
filters = args.get('filters', {})
16-
bastion_filters = args.get('bastion', {})
15+
filters = args.get('filters', [])
16+
bastion_filters = args.get('bastion', [])
1717

1818
if args.get('cluster') and not args.get('filters'):
19-
filters['tag:cluster'] = args.get('cluster')
19+
filters = [{'Name': 'tag:cluster', 'Values': [args.get('cluster')]}]
2020

2121
if args.get('cluster') and not args.get('bastion'):
22-
bastion_filters['tag:cluster'] = args.get('cluster')
23-
bastion_filters['tag:role'] = 'bastion'
22+
bastion_filters = [
23+
{'Name': 'tag:cluster', 'Values': [args.get('cluster')]},
24+
{'Name': 'tag:role', 'Values': ['bastion']}
25+
]
2426

2527
return Ec2Inventory(boto_profile=args['boto_profile'],
2628
regions=args['region'],

0 commit comments

Comments
 (0)