|
9 | 9 | from chaoslib.exceptions import ActivityFailed, FailedActivity
|
10 | 10 | from chaoslib.types import Configuration, Secrets
|
11 | 11 |
|
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 | +) |
13 | 18 | from chaosaws.types import AWSResponse
|
14 | 19 |
|
15 | 20 | __all__ = [
|
|
22 | 27 | "detach_random_volume",
|
23 | 28 | "attach_volume",
|
24 | 29 | "stop_instances_by_incremental_steps",
|
| 30 | + "set_tags_on_instances", |
| 31 | + "remove_tags_from_instances", |
25 | 32 | ]
|
26 | 33 |
|
27 | 34 | logger = get_logger()
|
@@ -496,6 +503,107 @@ def stop_instances_by_incremental_steps(
|
496 | 503 | return responses
|
497 | 504 |
|
498 | 505 |
|
| 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 | + |
499 | 607 | ###############################################################################
|
500 | 608 | # Private functions
|
501 | 609 | ###############################################################################
|
@@ -604,7 +712,7 @@ def get_instance_type_from_response(response: Dict) -> Dict:
|
604 | 712 | """
|
605 | 713 | Transform list of instance IDs to a dict with IDs by instance type
|
606 | 714 | """
|
607 |
| - instances_type = defaultdict(List) |
| 715 | + instances_type = defaultdict(list) |
608 | 716 | # reservations are instances that were started together
|
609 | 717 |
|
610 | 718 | for reservation in response["Reservations"]:
|
|
0 commit comments