|  | 
| 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