# -*- coding: utf-8 -*- import random from typing import Dict, List import boto3 from botocore.exceptions import ClientError from logzero import logger from chaoslib.exceptions import FailedActivity from chaoslib.types import Configuration, Secrets from chaosaws import aws_client from chaosaws.types import AWSResponse __all__ = ["deregister_target", "set_security_groups", "set_subnets", "delete_load_balancer"] def deregister_target(tg_name: str, configuration: Configuration = None, secrets: Secrets = None) -> AWSResponse: """Deregisters one random target from target group""" client = aws_client('elbv2', configuration, secrets) tg_arn = get_target_group_arns(tg_names=[tg_name], client=client) tg_health = get_targets_health_description(tg_arns=tg_arn, client=client) random_target = random.choice( tg_health[tg_name]['TargetHealthDescriptions']) logger.debug("Deregistering target {} from target group {}".format( random_target['Target']['Id'], tg_name)) try: return client.deregister_targets( TargetGroupArn=tg_arn[tg_name], Targets=[{ 'Id': random_target['Target']['Id'], 'Port': random_target['Target']['Port'] }] ) except ClientError as e: raise FailedActivity('Exception detaching %s: %s' % ( tg_name, e.response['Error']['Message'])) def set_security_groups(load_balancer_names: List[str], security_group_ids: List[str], configuration: Configuration = None, secrets: Secrets = None) -> List[AWSResponse]: """ Changes the security groups for the specified load balancer(s). This action will replace the existing security groups on an application load balancer with the specified security groups. Parameters: - load_balancer_names: a list of load balancer names - security_group_ids: a list of security group ids returns: [ { 'LoadBalancerArn': 'string', 'SecurityGroupIds': ['sg-0000000', 'sg-0000001'] }, ... ] """ security_group_ids = get_security_groups( security_group_ids, aws_client('ec2', configuration, secrets)) client = aws_client('elbv2', configuration, secrets) load_balancers = get_load_balancer_arns(load_balancer_names, client) if load_balancers.get('network', []): raise FailedActivity( 'Cannot change security groups of network load balancers.') results = [] for l in load_balancers['application']: response = client.set_security_groups( LoadBalancerArn=l, SecurityGroups=security_group_ids) # add load balancer arn to response response['LoadBalancerArn'] = l results.append(response) return results def set_subnets(load_balancer_names: List[str], subnet_ids: List[str], configuration: Configuration = None, secrets: Secrets = None) -> List[AWSResponse]: """ Changes the subnets for the specified application load balancer(s) This action will replace the existing security groups on an application load balancer with the specified security groups. Parameters: - load_balancer_names: a list of load balancer names - subnet_ids: a list of subnet ids returns: [ { 'LoadBalancerArn': 'string', 'AvailabilityZones': { 'ZoneName': 'string', 'SubnetId': 'string', 'LoadBalancerAddresses': [ { 'IpAddress': 'string', 'AllocationId': 'string' } ] } }, ... ] """ subnet_ids = get_subnets( subnet_ids, aws_client('ec2', configuration, secrets)) client = aws_client('elbv2', configuration, secrets) load_balancers = get_load_balancer_arns(load_balancer_names, client) if load_balancers.get('network', []): raise FailedActivity( 'Cannot change subnets of network load balancers.') results = [] for l in load_balancers['application']: response = client.set_subnets( LoadBalancerArn=l, Subnets=subnet_ids) response['LoadBalancerArn'] = l results.append(response) return results def delete_load_balancer(load_balancer_names: List[str], configuration: Configuration = None, secrets: Secrets = None): """ Deletes the provided load balancer(s). Parameters: - load_balancer_names: a list of load balancer names """ client = aws_client('elbv2', configuration, secrets) load_balancers = get_load_balancer_arns(load_balancer_names, client) for k, v in load_balancers.items(): if k not in ('application', 'network'): continue for l in v: logger.debug('Deleting load balancer %s' % l) client.delete_load_balancer(LoadBalancerArn=l) ############################################################################### # Private functions ############################################################################### def get_load_balancer_arns(load_balancer_names: List[str], client: boto3.client) -> Dict[str, List[str]]: """ Returns load balancer arns categorized by the type of load balancer return structure: { 'network': ['load balancer arn'], 'application': ['load balancer arn'] } """ results = {} logger.debug('Searching for load balancer name(s): {}.'.format( load_balancer_names)) try: response = client.describe_load_balancers( Names=load_balancer_names) for lb in response['LoadBalancers']: if lb['State']['Code'] != 'active': raise FailedActivity( 'Invalid state for load balancer {}: ' '{} is not active'.format( lb['LoadBalancerName'], lb['State']['Code'])) results.setdefault(lb['Type'], []).append( lb['LoadBalancerArn']) results.setdefault('Names', []).append(lb['LoadBalancerName']) except ClientError as e: raise FailedActivity(e.response['Error']['Message']) missing_lbs = [l for l in load_balancer_names if l not in results['Names']] if missing_lbs: raise FailedActivity( 'Unable to locate load balancer(s): {}'.format(missing_lbs)) if not results: raise FailedActivity( 'Unable to find any load balancer(s) matching name(s): {}'.format( load_balancer_names)) return results def get_target_group_arns(tg_names: List[str], client: boto3.client) -> Dict: """ Return list of target group ARNs based on list of target group names return structure: { "TargetGroupName": "TargetGroupArn", .... } """ logger.debug("Target group name(s): {} Looking for ARN" .format(str(tg_names))) res = client.describe_target_groups(Names=tg_names) tg_arns = {} for tg in res['TargetGroups']: tg_arns[tg['TargetGroupName']] = tg['TargetGroupArn'] logger.debug("Target groups ARN: {}".format(str(tg_arns))) return tg_arns def get_targets_health_description(tg_arns: Dict, client: boto3.client) -> Dict: """ Return TargetHealthDescriptions by targetgroups Structure: { "TargetGroupName": { "TargetGroupArn": value, "TargetHealthDescriptions": TargetHealthDescriptions[] }, .... } """ logger.debug("Target group ARN: {} Getting health descriptions" .format(str(tg_arns))) tg_health_descr = {} for tg in tg_arns: tg_health_descr[tg] = {} tg_health_descr[tg]['TargetGroupArn'] = tg_arns[tg] tg_health_descr[tg]['TargetHealthDescriptions'] = \ client.describe_target_health(TargetGroupArn=tg_arns[tg])[ 'TargetHealthDescriptions'] logger.debug("Health descriptions for target group(s) are: {}" .format(str(tg_health_descr))) return tg_health_descr def get_security_groups(sg_ids: List[str], client: boto3.client) -> List[str]: try: response = client.describe_security_groups( GroupIds=sg_ids)['SecurityGroups'] results = [r['GroupId'] for r in response] except ClientError as e: raise FailedActivity(e.response['Error']['Message']) missing_sgs = [s for s in sg_ids if s not in results] if missing_sgs: raise FailedActivity('Invalid security group id(s): {}'.format( missing_sgs)) return results def get_subnets(subnet_ids: List[str], client: boto3.client) -> List[str]: try: response = client.describe_subnets(SubnetIds=subnet_ids)['Subnets'] results = [r['SubnetId'] for r in response] except ClientError as e: raise FailedActivity(e.response['Error']['Message']) missing_subnets = [s for s in subnet_ids if s not in results] if missing_subnets: raise FailedActivity('Invalid subnet id(s): {}'.format( missing_subnets)) return results