Skip to content

Commit 4944bb1

Browse files
committed
add set_tags_on_instances and remove_tags_from_instances
Signed-off-by: Sylvain Hellegouarch <[email protected]>
1 parent dfb900d commit 4944bb1

File tree

4 files changed

+167
-3
lines changed

4 files changed

+167
-3
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55
[Unreleased]: https://github.com/chaostoolkit-incubator/chaostoolkit-aws/compare/0.32.1...HEAD
66

7+
### Added
8+
9+
* The `set_tags_on_instances` and `remove_tags_from_instances` actions on the
10+
EC2 package
11+
712
## [0.32.1][] - 2024-02-23
813

914
[0.32.1]: https://github.com/chaostoolkit-incubator/chaostoolkit-aws/compare/0.32.0...0.32.1

chaosaws/ec2/actions.py

+110-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
from chaoslib.exceptions import ActivityFailed, FailedActivity
1010
from chaoslib.types import Configuration, Secrets
1111

12-
from chaosaws import aws_client, convert_tags, get_logger
12+
from chaosaws import (
13+
aws_client,
14+
convert_tags,
15+
get_logger,
16+
tags_as_key_value_pairs,
17+
)
1318
from chaosaws.types import AWSResponse
1419

1520
__all__ = [
@@ -22,6 +27,8 @@
2227
"detach_random_volume",
2328
"attach_volume",
2429
"stop_instances_by_incremental_steps",
30+
"set_tags_on_instances",
31+
"remove_tags_from_instances",
2532
]
2633

2734
logger = get_logger()
@@ -496,6 +503,107 @@ def stop_instances_by_incremental_steps(
496503
return responses
497504

498505

506+
def set_tags_on_instances(
507+
tags: Union[str, List[Dict[str, str]]],
508+
percentage: int = 100,
509+
az: str = None,
510+
filters: List[Dict[str, Any]] = None,
511+
configuration: Configuration = None,
512+
secrets: Secrets = None,
513+
) -> AWSResponse:
514+
"""
515+
Sets some tags on the instances matching the `filters`. The set of instances
516+
may be filtered down by availability-zone too.
517+
518+
The `tags`can be passed as a dictionary of key, value pair respecting
519+
the usual AWS form: [{"Key": "...", "Value": "..."}, ...] or as a string
520+
of key value pairs such as "k1=v1,k2=v2"
521+
522+
The `percentage` parameter (between 0 and 100) allows you to select only a
523+
certain amount of instances amongst those matching the filters.
524+
525+
If no filters are given and `percentage` remains to 100, the entire set
526+
of instances in an AZ will be tagged. If no AZ is provided, your entire
527+
set of instances in the region will be tagged. This can be a lot of
528+
instances and would not be appropriate. Always to use the filters to
529+
target a significant subset.
530+
531+
See also: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2/client/create_tags.html
532+
""" # noqa E501
533+
client = aws_client("ec2", configuration, secrets)
534+
535+
if isinstance(tags, str):
536+
tags = tags_as_key_value_pairs(convert_tags(tags) if tags else [])
537+
538+
if not tags:
539+
raise FailedActivity("Missing tags to be set")
540+
541+
filters = filters or []
542+
if az:
543+
filters.append({"Name": "availability-zone", "Values": [az]})
544+
545+
instances = list_instances_by_type(filters, client)
546+
547+
instance_ids = [inst_id for inst_id in instances.get("normal", [])]
548+
549+
total = len(instance_ids)
550+
# always force at least one instance
551+
count = max(1, round(total * percentage / 100))
552+
target_instances = random.sample(instance_ids, count)
553+
554+
if not target_instances:
555+
raise FailedActivity(f"No instances in availability zone: {az}")
556+
557+
logger.debug(
558+
"Picked EC2 instances '{}' from AZ '{}'".format(
559+
str(target_instances), az
560+
)
561+
)
562+
563+
response = client.create_tags(Resources=target_instances, Tags=tags)
564+
565+
return response
566+
567+
568+
def remove_tags_from_instances(
569+
tags: Union[str, List[Dict[str, str]]],
570+
az: str = None,
571+
configuration: Configuration = None,
572+
secrets: Secrets = None,
573+
) -> AWSResponse:
574+
"""
575+
Remove tags from instances
576+
577+
Usually mirrors `set_tags_on_instances`.
578+
"""
579+
client = aws_client("ec2", configuration, secrets)
580+
581+
if isinstance(tags, str):
582+
tags = tags_as_key_value_pairs(convert_tags(tags) if tags else [])
583+
584+
filters = []
585+
for tag in tags:
586+
filters.append({"Name": f"tag:{tag['Key']}", "Values": [tag["Value"]]})
587+
588+
if az:
589+
filters.append({"Name": "availability-zone", "Values": [az]})
590+
591+
instances = client.describe_instances(Filters=filters)
592+
593+
instance_ids = []
594+
for reservation in instances["Reservations"]:
595+
for inst in reservation["Instances"]:
596+
instance_ids.append(inst["InstanceId"])
597+
598+
logger.debug(
599+
"Found EC2 instances '{}' from AZ '{}'".format(str(instance_ids), az)
600+
)
601+
602+
response = client.delete_tags(Resources=instance_ids, Tags=tags)
603+
604+
return response
605+
606+
499607
###############################################################################
500608
# Private functions
501609
###############################################################################
@@ -604,7 +712,7 @@ def get_instance_type_from_response(response: Dict) -> Dict:
604712
"""
605713
Transform list of instance IDs to a dict with IDs by instance type
606714
"""
607-
instances_type = defaultdict(List)
715+
instances_type = defaultdict(list)
608716
# reservations are instances that were started together
609717

610718
for reservation in response["Reservations"]:

tests/conftest.py

-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,4 @@
1212

1313
@pytest.fixture(scope="session", autouse=True)
1414
def setup_logger() -> None:
15-
print("#######################")
1615
configure_logger(verbose=True)

tests/ec2/test_ec2_actions.py

+52
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
attach_volume,
99
authorize_security_group_ingress,
1010
detach_random_volume,
11+
remove_tags_from_instances,
1112
restart_instances,
1213
revoke_security_group_ingress,
14+
set_tags_on_instances,
1315
start_instances,
1416
stop_instance,
1517
stop_instances,
@@ -1134,3 +1136,53 @@ def test_revoke_security_group_ingress_with_cidr_ip(aws_client):
11341136
}
11351137
],
11361138
)
1139+
1140+
1141+
@patch("chaosaws.ec2.actions.aws_client", autospec=True)
1142+
def test_set_tags_on_instances(aws_client):
1143+
tags = "a=b,c=d"
1144+
expected_tags = [{"Key": "a", "Value": "b"}, {"Key": "c", "Value": "d"}]
1145+
1146+
client = MagicMock()
1147+
aws_client.return_value = client
1148+
inst_id_1 = "i-1234567890abcdef0"
1149+
client.describe_instances.return_value = {
1150+
"Reservations": [
1151+
{
1152+
"Instances": [
1153+
{"InstanceId": inst_id_1, "InstanceLifecycle": "normal"}
1154+
]
1155+
}
1156+
]
1157+
}
1158+
1159+
set_tags_on_instances(tags, percentage=10)
1160+
1161+
client.create_tags.assert_called_with(
1162+
Resources=[inst_id_1], Tags=expected_tags
1163+
)
1164+
1165+
1166+
@patch("chaosaws.ec2.actions.aws_client", autospec=True)
1167+
def test_remove_tags_from_instances(aws_client):
1168+
tags = "a=b,c=d"
1169+
expected_tags = [{"Key": "a", "Value": "b"}, {"Key": "c", "Value": "d"}]
1170+
1171+
client = MagicMock()
1172+
aws_client.return_value = client
1173+
inst_id_1 = "i-1234567890abcdef0"
1174+
client.describe_instances.return_value = {
1175+
"Reservations": [
1176+
{
1177+
"Instances": [
1178+
{"InstanceId": inst_id_1, "InstanceLifecycle": "normal"}
1179+
]
1180+
}
1181+
]
1182+
}
1183+
1184+
remove_tags_from_instances(tags)
1185+
1186+
client.delete_tags.assert_called_with(
1187+
Resources=[inst_id_1], Tags=expected_tags
1188+
)

0 commit comments

Comments
 (0)