Skip to content

Commit ddd1689

Browse files
Dmitry Batiievskyirussmiles
Dmitry Batiievskyi
authored andcommitted
asg paginator support and fix for 'inservice' instances (#21)
Signed-off-by: Dmitry Batiievskyi <dmitriy@batiyevsky.org>
1 parent 6c4859e commit ddd1689

File tree

6 files changed

+132
-88
lines changed

6 files changed

+132
-88
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
- support for elbv2 with basic probes and action
1010
- support for asg with basic probes
11+
- fix asg probe to support pagination
1112

1213
## [0.7.0][]
1314

chaosaws/asg/probes.py

+15-10
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
from typing import Any, Dict, List
44

55
import boto3
6-
from logzero import logger
7-
86
from chaosaws import aws_client
97
from chaosaws.types import AWSResponse
108
from chaoslib.exceptions import FailedActivity
119
from chaoslib.types import Configuration, Secrets
10+
from logzero import logger
1211

1312
__all__ = ["desired_equals_healthy", "desired_equals_healthy_tags"]
1413

@@ -61,23 +60,30 @@ def desired_equals_healthy_tags(tags: List[Dict[str, str]],
6160

6261
client = aws_client('autoscaling', configuration, secrets)
6362

64-
asg_descrs = client.describe_auto_scaling_groups()
65-
6663
# The following is needed because AWS API does not support filters
6764
# on auto-scaling groups
6865

66+
# fetch all ASGs using paginator
67+
page_iterator = client.get_paginator(
68+
'describe_auto_scaling_groups').paginate(
69+
PaginationConfig={'PageSize': 100})
70+
asg_descrs = {'AutoScalingGroups': []}
71+
72+
for page in page_iterator:
73+
asg_descrs['AutoScalingGroups'].extend(page['AutoScalingGroups'])
74+
6975
filter_set = set(map(lambda x: "=".join([x['Key'], x['Value']]), tags))
7076

71-
group_sets = map(lambda g: {
77+
group_sets = list(map(lambda g: {
7278
'Name': g['AutoScalingGroupName'],
7379
'Tags': set(map(
7480
lambda t: "=".join([t['Key'], t['Value']]), g['Tags'])
75-
)}, asg_descrs['AutoScalingGroups'])
81+
)}, asg_descrs['AutoScalingGroups']))
7682

7783
filtered_groups = [g['Name']
7884
for g in group_sets if filter_set.issubset(g['Tags'])]
7985

80-
logger.debug("filtered_groups: {}".format(filtered_groups))
86+
logger.debug("filtered groups: {}".format(filtered_groups))
8187

8288
if filtered_groups:
8389
groups_descr = client.describe_auto_scaling_groups(
@@ -86,8 +92,6 @@ def desired_equals_healthy_tags(tags: List[Dict[str, str]],
8692
raise FailedActivity(
8793
"No auto-scaling groups matched the tags provided")
8894

89-
logger.debug("groups_descr: {}".format(groups_descr))
90-
9195
return is_desired_equals_healthy(groups_descr)
9296

9397

@@ -101,7 +105,8 @@ def is_desired_equals_healthy(groups_descr: Dict):
101105
healthy_cnt = Counter()
102106

103107
for instance in group_descr['Instances']:
104-
healthy_cnt[instance['HealthStatus']] += 1
108+
if instance['LifecycleState'] == 'InService':
109+
healthy_cnt[instance['HealthStatus']] += 1
105110

106111
if healthy_cnt['Healthy']:
107112
if group_descr['DesiredCapacity'] == healthy_cnt['Healthy']:

chaosaws/ec2/actions.py

+23-12
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
# -*- coding: utf-8 -*-
2-
from copy import deepcopy
32
import random
3+
from collections import defaultdict
4+
from copy import deepcopy
45
from typing import Any, Dict, List
56

67
import boto3
7-
from chaoslib.exceptions import FailedActivity
8-
from chaoslib.types import Configuration, Secrets
98
from logzero import logger
109

1110
from chaosaws import aws_client
1211
from chaosaws.types import AWSResponse
13-
14-
from collections import defaultdict
15-
12+
from chaoslib.exceptions import FailedActivity
13+
from chaoslib.types import Configuration, Secrets
1614

1715
__all__ = ["stop_instance", "stop_instances"]
1816

@@ -29,6 +27,7 @@ def stop_instance(instance_id: str = None, az: str = None, force: bool = False,
2927
also provide a list of filters following the documentation
3028
https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.describe_instances
3129
"""
30+
3231
if not az and not instance_id and not filters:
3332
raise FailedActivity(
3433
"To stop an EC2 instance, you must specify either the instance id,"
@@ -38,6 +37,7 @@ def stop_instance(instance_id: str = None, az: str = None, force: bool = False,
3837

3938
if not instance_id:
4039
filters = deepcopy(filters) if filters else []
40+
4141
if az:
4242
filters.append({'Name': 'availability-zone', 'Values': [az]})
4343
instance_types = pick_random_instance(filters, client)
@@ -58,6 +58,7 @@ def stop_instance(instance_id: str = None, az: str = None, force: bool = False,
5858

5959
def stop_instances(instance_ids: List[str] = None, az: str = None,
6060
filters: List[Dict[str, Any]] = None,
61+
6162
force: bool = False, configuration: Configuration = None,
6263
secrets: Secrets = None) -> AWSResponse:
6364
"""
@@ -66,6 +67,7 @@ def stop_instances(instance_ids: List[str] = None, az: str = None,
6667
also provide a list of filters following the documentation
6768
https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.describe_instances
6869
"""
70+
6971
if not az and not instance_ids and not filters:
7072
raise FailedActivity(
7173
"To stop EC2 instances, you must specify either the instance ids,"
@@ -79,6 +81,7 @@ def stop_instances(instance_ids: List[str] = None, az: str = None,
7981

8082
if not instance_ids:
8183
filters = deepcopy(filters) if filters else []
84+
8285
if az:
8386
filters.append({'Name': 'availability-zone', 'Values': [az]})
8487
instance_types = list_instances_by_type(filters, client)
@@ -121,7 +124,8 @@ def pick_random_instance(filters: List[Dict[str, Any]],
121124
"""
122125
instances_type = list_instances_by_type(filters, client)
123126
random_id = random.choice([item for sublist in instances_type.values()
124-
for item in sublist])
127+
for item in sublist])
128+
125129
for inst_type in instances_type:
126130
if random_id in instances_type[inst_type]:
127131
return {inst_type: [random_id]}
@@ -133,13 +137,15 @@ def get_instance_type_from_response(response: Dict) -> Dict:
133137
"""
134138
instances_type = defaultdict(List)
135139
# reservations are instances that were started together
140+
136141
for reservation in response['Reservations']:
137142
for inst in reservation['Instances']:
138143
if inst['InstanceLifecycle'] not in instances_type.keys():
139144
# adding empty list (value) for new instance type (key)
140145
instances_type[inst['InstanceLifecycle']] = []
141146
instances_type[inst['InstanceLifecycle']].append(
142-
inst['InstanceId'])
147+
inst['InstanceId'])
148+
143149
return instances_type
144150

145151

@@ -149,10 +155,12 @@ def get_spot_request_ids_from_response(response: Dict) -> List[str]:
149155
(DescribeInstances)
150156
"""
151157
spot_request_ids = []
158+
152159
for reservation in response['Reservations']:
153160
for inst in reservation['Instances']:
154161
if inst['InstanceLifecycle'] == 'spot':
155162
spot_request_ids.append(inst['SpotInstanceRequestId'])
163+
156164
return spot_request_ids
157165

158166

@@ -163,32 +171,35 @@ def get_instance_type_by_id(instance_ids: List[str],
163171
"""
164172
instances_type = defaultdict(List)
165173
res = client.describe_instances(InstanceIds=instance_ids)
174+
166175
return get_instance_type_from_response(res)
167176

168177

169178
def stop_instances_any_type(instance_types: dict, force, client: boto3.client):
170179
"""
171180
Stop instances regardless of the instance type (ondemand, spot)
172181
"""
182+
173183
if 'normal' in instance_types:
174184
logger.debug("Stopping instances: {}".format(instance_types['normal']))
175185
client.stop_instances(InstanceIds=instance_types['normal'],
176186
Force=force)
187+
177188
if 'spot' in instance_types:
178189
# TODO: proper support for spot fleets
179190
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-fleet.html
180191

181192
# To properly stop spot instances have to cancel spot requests first
182193
spot_request_ids = get_spot_request_ids_from_response(
183-
client.describe_instances(InstanceIds=instance_types['spot']))
194+
client.describe_instances(InstanceIds=instance_types['spot']))
184195

185196
logger.debug("Canceling spot requests: {}".format(spot_request_ids))
186197
client.cancel_spot_instance_requests(
187-
SpotInstanceRequestIds=spot_request_ids)
198+
SpotInstanceRequestIds=spot_request_ids)
188199
logger.debug("Terminating spot instances: {}".format(
189200
instance_types['spot']))
190-
client.terminate_instances(InstanceIds=instance_types['spot'],
191-
Force=force)
201+
client.terminate_instances(InstanceIds=instance_types['spot'])
202+
192203
if 'scheduled' in instance_types:
193204
# TODO: add support for scheduled inststances
194205
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-scheduled-instances.html

chaosaws/ec2/probes.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
# -*- coding: utf-8 -*-
22
from typing import Any, Dict, List
33

4-
from chaoslib.types import Configuration, Secrets
5-
64
from chaosaws import aws_client
75
from chaosaws.types import AWSResponse
6+
from chaoslib.types import Configuration, Secrets
87

98
__all__ = ["describe_instances", "count_instances"]
109

@@ -16,9 +15,11 @@ def describe_instances(filters: List[Dict[str, Any]],
1615
Describe instances following the specified filters.
1716
1817
Please refer to http://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.describe_instances
18+
1919
for details on said filters.
2020
""" # noqa: E501
2121
client = aws_client('ec2', configuration, secrets)
22+
2223
return client.describe_instances(Filters=filters)
2324

2425

@@ -29,8 +30,10 @@ def count_instances(filters: List[Dict[str, Any]],
2930
Return count of instances matching the specified filters.
3031
3132
Please refer to http://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.describe_instances
33+
3234
for details on said filters.
3335
""" # noqa: E501
3436
client = aws_client('ec2', configuration, secrets)
3537
result = client.describe_instances(Filters=filters)
36-
return (len(result['Reservations']))
38+
39+
return len(result['Reservations'])

0 commit comments

Comments
 (0)