Skip to content

Commit 6c4859e

Browse files
Dmitry Batiievskyirussmiles
Dmitry Batiievskyi
authored andcommitted
asg support with basic probes (#18)
Signed-off-by: Dmitry Batiievskyi <dmitriy@batiyevsky.org>
1 parent 279ecc0 commit 6c4859e

File tree

4 files changed

+308
-9
lines changed

4 files changed

+308
-9
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
### Added
88

99
- support for elbv2 with basic probes and action
10+
- support for asg with basic probes
1011

1112
## [0.7.0][]
1213

chaosaws/__init__.py

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

4+
import boto3
5+
import requests
46
from aws_requests_auth.aws_auth import AWSRequestsAuth
57
from aws_requests_auth.boto_utils import BotoAWSRequestsAuth
6-
import boto3
78
from botocore import parsers
8-
from chaoslib.discovery.discover import discover_actions, discover_probes, \
9-
initialize_discovery_result
9+
from chaosaws.types import AWSResponse
10+
from chaoslib.discovery.discover import (discover_actions, discover_probes,
11+
initialize_discovery_result)
1012
from chaoslib.exceptions import DiscoveryFailed
11-
from chaoslib.types import Configuration, Discovery, DiscoveredActivities, \
12-
DiscoveredSystemInfo, Secrets
13+
from chaoslib.types import (Configuration, DiscoveredActivities,
14+
DiscoveredSystemInfo, Discovery, Secrets)
1315
from logzero import logger
14-
import requests
15-
16-
from chaosaws.types import AWSResponse
17-
1816

1917
__version__ = '0.7.0'
2018
__all__ = ["__version__", "discover", "aws_client", "signed_api_call"]
@@ -31,6 +29,7 @@ def get_credentials(secrets: Secrets = None) -> Dict[str, str]:
3129
creds = dict(
3230
aws_access_key_id=None, aws_secret_access_key=None,
3331
aws_session_token=None)
32+
3433
if secrets:
3534
creds["aws_access_key_id"] = secrets.get("aws_access_key_id")
3635
creds["aws_secret_access_key"] = secrets.get("aws_secret_access_key")
@@ -80,6 +79,7 @@ def signed_api_call(service: str, path: str = "/", method: str = 'GET',
8079
8180
This should only be used when boto does not already implement the service
8281
itself. See https://boto3.readthedocs.io/en/latest/reference/services/index.html
82+
8383
for a list of supported services by boto. This function does not claim
8484
being generic enough to support the whole range of AWS API.
8585
@@ -122,6 +122,7 @@ def signed_api_call(service: str, path: str = "/", method: str = 'GET',
122122

123123
# when creds weren't provided via secrets, we let boto search for them
124124
# from the process environment
125+
125126
if creds["aws_access_key_id"] and creds["aws_secret_access_key"]:
126127
auth = AWSRequestsAuth(
127128
aws_access_key=creds["aws_access_key_id"],
@@ -179,4 +180,6 @@ def load_exported_activities() -> List[DiscoveredActivities]:
179180
activities.extend(discover_probes("chaosaws.eks.probes"))
180181
activities.extend(discover_actions("chaosaws.elbv2.actions"))
181182
activities.extend(discover_probes("chaosaws.elbv2.probes"))
183+
activities.extend(discover_probes("chaosaws.asg.probes"))
184+
182185
return activities

chaosaws/asg/probes.py

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# -*- coding: utf-8 -*-
2+
from collections import Counter
3+
from typing import Any, Dict, List
4+
5+
import boto3
6+
from logzero import logger
7+
8+
from chaosaws import aws_client
9+
from chaosaws.types import AWSResponse
10+
from chaoslib.exceptions import FailedActivity
11+
from chaoslib.types import Configuration, Secrets
12+
13+
__all__ = ["desired_equals_healthy", "desired_equals_healthy_tags"]
14+
15+
16+
def desired_equals_healthy(asg_names: List[str],
17+
configuration: Configuration = None,
18+
secrets: Secrets = None) -> AWSResponse:
19+
"""
20+
If desired number matches the number of healthy instances
21+
22+
for each of the auto-scaling groups
23+
24+
Returns: bool
25+
"""
26+
27+
if not asg_names:
28+
raise FailedActivity(
29+
"Non-empty list of auto scaling groups is required")
30+
31+
client = aws_client('autoscaling', configuration, secrets)
32+
33+
groups_descr = client.describe_auto_scaling_groups(
34+
AutoScalingGroupNames=asg_names)
35+
36+
return is_desired_equals_healthy(groups_descr)
37+
38+
39+
def desired_equals_healthy_tags(tags: List[Dict[str, str]],
40+
configuration: Configuration = None,
41+
secrets: Secrets = None) -> AWSResponse:
42+
"""
43+
If desired number matches the number of healthy instances
44+
45+
for each of the auto-scaling groups matching tags provided
46+
47+
`tags` are expected as:
48+
[{
49+
'Key': 'KeyName',
50+
'Value': 'KeyValue'
51+
},
52+
...
53+
]
54+
55+
Returns: bool
56+
"""
57+
58+
if not tags:
59+
raise FailedActivity(
60+
"Non-empty tags is required")
61+
62+
client = aws_client('autoscaling', configuration, secrets)
63+
64+
asg_descrs = client.describe_auto_scaling_groups()
65+
66+
# The following is needed because AWS API does not support filters
67+
# on auto-scaling groups
68+
69+
filter_set = set(map(lambda x: "=".join([x['Key'], x['Value']]), tags))
70+
71+
group_sets = map(lambda g: {
72+
'Name': g['AutoScalingGroupName'],
73+
'Tags': set(map(
74+
lambda t: "=".join([t['Key'], t['Value']]), g['Tags'])
75+
)}, asg_descrs['AutoScalingGroups'])
76+
77+
filtered_groups = [g['Name']
78+
for g in group_sets if filter_set.issubset(g['Tags'])]
79+
80+
logger.debug("filtered_groups: {}".format(filtered_groups))
81+
82+
if filtered_groups:
83+
groups_descr = client.describe_auto_scaling_groups(
84+
AutoScalingGroupNames=filtered_groups)
85+
else:
86+
raise FailedActivity(
87+
"No auto-scaling groups matched the tags provided")
88+
89+
logger.debug("groups_descr: {}".format(groups_descr))
90+
91+
return is_desired_equals_healthy(groups_descr)
92+
93+
94+
###############################################################################
95+
# Private functions
96+
###############################################################################
97+
def is_desired_equals_healthy(groups_descr: Dict):
98+
desired_equals_healthy = False
99+
100+
for group_descr in groups_descr['AutoScalingGroups']:
101+
healthy_cnt = Counter()
102+
103+
for instance in group_descr['Instances']:
104+
healthy_cnt[instance['HealthStatus']] += 1
105+
106+
if healthy_cnt['Healthy']:
107+
if group_descr['DesiredCapacity'] == healthy_cnt['Healthy']:
108+
desired_equals_healthy = True
109+
else:
110+
desired_equals_healthy = False
111+
112+
break
113+
else:
114+
desired_equals_healthy = False
115+
116+
break
117+
118+
return desired_equals_healthy

tests/asg/test_asg_probes.py

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# -*- coding: utf-8 -*-
2+
from unittest.mock import MagicMock, patch
3+
4+
import pytest
5+
6+
from chaosaws.asg.probes import (desired_equals_healthy,
7+
desired_equals_healthy_tags)
8+
from chaoslib.exceptions import FailedActivity
9+
10+
11+
@patch('chaosaws.asg.probes.aws_client', autospec=True)
12+
def test_desired_equals_healthy_true(aws_client):
13+
client = MagicMock()
14+
aws_client.return_value = client
15+
asg_names = ['AutoScalingGroup1', 'AutoScalingGroup2']
16+
client.describe_auto_scaling_groups.return_value = {
17+
"AutoScalingGroups": [{
18+
"DesiredCapacity": 1,
19+
"Instances": [{
20+
"HealthStatus": "Healthy"
21+
}]
22+
}]
23+
}
24+
assert desired_equals_healthy(asg_names=asg_names) is True
25+
26+
27+
@patch('chaosaws.asg.probes.aws_client', autospec=True)
28+
def test_desired_equals_healthy_false(aws_client):
29+
client = MagicMock()
30+
aws_client.return_value = client
31+
asg_names = ['AutoScalingGroup1', 'AutoScalingGroup2']
32+
client.describe_auto_scaling_groups.return_value = {
33+
"AutoScalingGroups": [{
34+
"DesiredCapacity": 1,
35+
"Instances": [{
36+
"HealthStatus": "Unhealthy"
37+
}]
38+
}]
39+
}
40+
assert desired_equals_healthy(asg_names=asg_names) is False
41+
42+
43+
@patch('chaosaws.asg.probes.aws_client', autospec=True)
44+
def test_desired_equals_healthy_empty(aws_client):
45+
client = MagicMock()
46+
aws_client.return_value = client
47+
asg_names = ['AutoScalingGroup1', 'AutoScalingGroup2']
48+
client.describe_auto_scaling_groups.return_value = {
49+
"AutoScalingGroups": [
50+
]
51+
}
52+
assert desired_equals_healthy(asg_names=asg_names) is False
53+
54+
55+
def test_desired_equals_healthy_needs_asg_names():
56+
with pytest.raises(FailedActivity) as x:
57+
desired_equals_healthy([])
58+
assert "Non-empty list of auto scaling groups is required" in str(x)
59+
60+
61+
def desired_equals_healthy_tags_true_sideeffect(*args, **kwargs):
62+
if 'AutoScalingGroupNames' not in kwargs:
63+
return {
64+
"AutoScalingGroups": [
65+
{
66+
'AutoScalingGroupName': 'AutoScalingGroup1',
67+
"DesiredCapacity": 1,
68+
"Instances": [{
69+
"HealthStatus": "Healthy"
70+
}],
71+
'Tags': [{
72+
'ResourceId': 'AutoScalingGroup1',
73+
'Key': 'Application',
74+
'Value': 'mychaosapp'
75+
}]
76+
},
77+
{
78+
'AutoScalingGroupName': 'AutoScalingGroup2',
79+
"DesiredCapacity": 1,
80+
"Instances": [{
81+
"HealthStatus": "Unhealthy"
82+
}],
83+
'Tags': [{
84+
'ResourceId': 'AutoScalingGroup1',
85+
'Key': 'Application',
86+
'Value': 'NOTmychaosapp'
87+
}]
88+
},
89+
{
90+
'AutoScalingGroupName': 'AutoScalingGroup3',
91+
"DesiredCapacity": 1,
92+
"Instances": [{
93+
"HealthStatus": "Unhealthy"
94+
}],
95+
'Tags': [{
96+
'ResourceId': 'AutoScalingGroup1',
97+
'Key': 'NOTApplication',
98+
'Value': 'mychaosapp'
99+
}]
100+
}
101+
]
102+
}
103+
else:
104+
return {
105+
"AutoScalingGroups": [
106+
{
107+
'AutoScalingGroupName': 'AutoScalingGroup1',
108+
"DesiredCapacity": 1,
109+
"Instances": [{
110+
"HealthStatus": "Healthy"
111+
}],
112+
'Tags': [{
113+
'ResourceId': 'AutoScalingGroup1',
114+
'Key': 'Application',
115+
'Value': 'mychaosapp'
116+
}]
117+
}
118+
]
119+
}
120+
121+
122+
@patch('chaosaws.asg.probes.aws_client', autospec=True)
123+
def test_desired_equals_healthy_tags_true(aws_client):
124+
client = MagicMock()
125+
aws_client.return_value = client
126+
tags = [{'Key': 'Application', 'Value': 'mychaosapp'}]
127+
client.describe_auto_scaling_groups.side_effect = \
128+
desired_equals_healthy_tags_true_sideeffect
129+
assert desired_equals_healthy_tags(tags=tags) is True
130+
131+
132+
@patch('chaosaws.asg.probes.aws_client', autospec=True)
133+
def test_desired_equals_healthy_tags_false(aws_client):
134+
client = MagicMock()
135+
aws_client.return_value = client
136+
tags = [{'Key': 'Application', 'Value': 'mychaosapp'}]
137+
client.describe_auto_scaling_groups.return_value = {
138+
"AutoScalingGroups": [
139+
{
140+
'AutoScalingGroupName': 'AutoScalingGroup1',
141+
"DesiredCapacity": 1,
142+
"Instances": [{
143+
"HealthStatus": "Unhealthy"
144+
}],
145+
'Tags': [{
146+
'ResourceId': 'AutoScalingGroup1',
147+
'Key': 'Application',
148+
'Value': 'mychaosapp'
149+
}]
150+
},
151+
{
152+
'AutoScalingGroupName': 'AutoScalingGroup2',
153+
"DesiredCapacity": 1,
154+
"Instances": [{
155+
"HealthStatus": "Unhealthy"
156+
}],
157+
'Tags': [{
158+
'ResourceId': 'AutoScalingGroup1',
159+
'Key': 'Application',
160+
'Value': 'NOTmychaosapp'
161+
}]
162+
},
163+
{
164+
'AutoScalingGroupName': 'AutoScalingGroup3',
165+
"DesiredCapacity": 1,
166+
"Instances": [{
167+
"HealthStatus": "Unhealthy"
168+
}],
169+
'Tags': [{
170+
'ResourceId': 'AutoScalingGroup1',
171+
'Key': 'NOTApplication',
172+
'Value': 'mychaosapp'
173+
}]
174+
}
175+
]
176+
}
177+
assert desired_equals_healthy_tags(tags=tags) is False

0 commit comments

Comments
 (0)