Exemple #1
0
def describe_vpc_cached(vpc_id: str, account_id: str, region: str,
                        **kwargs) -> dict:
    """
    Describe a specific VPC from our cache of VPC descriptions for a given account and region
    Return a dict if things go as expected.

    We accept additional arguments here to handle the case where the arguments are given as
    a dictionary with additional keys. Such as when filtering the main config for only VPCs
    which actually exist.

    :param vpc_id: A vpc id e.g. vpc-abc123
    :type vpc_id: str
    :param account_id: An AWS account id number
    :type account: str
    :param region: An AWS region where the service client should be created. e.g. us-east-1
    :type region: str
    :param **kwargs: Handle trailing arguments in case where dictionary of arguments is provided.
    :type **kwargs: dict
    :returns: A list of dictionaries representing all the VPCs for a given account and region.
    :rtype: list
    """
    LOGGER.debug(
        f'Fetching description of VPC {vpc_id} for account {account_id} in region {region}'
    )
    vpcs = describe_account_vpcs_cached(account_id, region)
    if vpc_dict := next((x for x in vpcs if x['VpcId'] == vpc_id), None):
        return vpc_dict
Exemple #2
0
def get_all_env_peerings(config: Sequence[dict], metadata: Mapping) -> List[dict]:
    """
    Given the configuration, find all the peerings which exist across all accounts
    and regions.
    See https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.describe_vpc_peering_connections
    :param config: A list containing dictionaries representing the configuration for the environment
    :type config: list
    :param key: The key to deduplicate on.
    :returns: A deduplicated list of all peerings for this environment.
    :rtype: list
    """
    # Get a dictionary of accounts, regions, vpcs and the vpcs they should be peered to
    config_peer_map = deepcopy(get_peering_map(config))
    # Initialise a filter to get vpc peerings relevant to the current environment being worked on
    filters = [{'Name': 'tag:peerd_created', 'Values': ['true']},
               {'Name': 'tag:peerd_environment', 'Values': [metadata['environment']]},
               {'Name': 'status-code', 'Values': ['active']}]
    # Get all existing peerings from accounts listed in the config file
    peerings: List[dict] = []
    for account_id in config_peer_map.keys():
        for region in config_peer_map[account_id].keys():
            peerings.extend(get_all_peerings(account_id, region, filters))
    # Now get all existing peerings from all accounts that participate in peerings
    # (There might be accounts not listed in the config file)
    for peering in peerings:
        for side in ('RequesterVpcInfo', 'AccepterVpcInfo'):
            account_id = peering[side]['OwnerId']
            region = peering[side]['Region']
            if not config_peer_map[account_id][region]:
                LOGGER.debug(f'Found peering to unconfigured account {account_id} region {region}. Inspecting...')
                peerings.extend(get_all_peerings(account_id, region, filters))
                config_peer_map[account_id][region] = True
                LOGGER.debug(f'Discovered additional peerings in {account_id} {region}')
    # Add the 2 lists, and ceduplicate peerings. These will be all existing peerings even between accounts deleted from the config file
    return deduplicate_list_dicts(peerings, 'VpcPeeringConnectionId')
Exemple #3
0
def aws_client(account: str, service: str, region: str):
    """
    Initialises a sts client, gets assume role credentials
    and initialises a AWS client connection to a requested resource.
    Returns the AWS product/resource client connection.
    Caches the client connection for re-use.
    For reference: http://boto3.readthedocs.io/en/latest/guide/configuration.html

    :param account: An AWS account id number
    :type account: str
    :param service: An AWS service e.g. ec2
    :type service: str
    :param region: An AWS region where the service client should be created. e.g. us-east-1
    :type region: str
    """

    global CLIENT_CACHE

    # In this block we get credentials for the target account by assuming
    # into the given account using a sts client connection
    # If we can't we return none and cache none.
    if not (credentials := get_role_credentials(account, aws_sts_client())):
        LOGGER.warning(
            f'Unable to tokenise into {account} for service {service} in region {region}. Moving on'
        )
        CLIENT_CACHE[account][region][service] = None
        return None
Exemple #4
0
def common_vpc_cidrs(peer_desc: list) -> bool:
    """
    Returns true if the VPCs share any CIDRs in common
    Returns false if the VPCs share no CIDRs in common

    Example Input:
    ```
    [
        {
            'account_id': '1234567890',
            'vpc_id': 'vpc-123',
            'region': 'us-west-2'
        }
        {
            'account_id': '3456789012',
            'vpc_id': 'vpc-456',
            'region': 'us-west-1'
        }
    ]
    ```

    :param peer_desc: A list containing two dictionaries describing the account, vpc and region of two sides of a peering
    :type peer_desc: list
    :returns: True if there 1 or more common CIDRs, False otherwise.
    """
    requester_vpc_cidrs = list_vpc_cidrs(peer_desc[0]['vpc_id'], peer_desc[0]['account_id'], peer_desc[0]['region'])
    accepter_vpc_cidrs = list_vpc_cidrs(peer_desc[1]['vpc_id'], peer_desc[1]['account_id'], peer_desc[1]['region'])
    if bool(set(requester_vpc_cidrs) & set(accepter_vpc_cidrs)):
        LOGGER.info(f'VPC {peer_desc[0]["vpc_id"]} cidrs {requester_vpc_cidrs} and {peer_desc[0]["vpc_id"]} cidrs {accepter_vpc_cidrs} share a common cidr.')
    return bool(set(requester_vpc_cidrs) & set(accepter_vpc_cidrs))
Exemple #5
0
def aws_sts_client() -> Any:
    """
    Uses default boto credentials locations, such as the instance metadata
    to return a sts client connection and caches it as a global variable for reuse.

    :returns: AWS STS client connection
    """

    sts_client = boto3.client('sts', config=Config(retries=dict(max_attempts=10)))
    LOGGER.info(f'Found the following STS identity:\n{json.dumps(sts_client.get_caller_identity(), indent=2)}')
    return sts_client
Exemple #6
0
def get_vpc_peering(vpc_id: str,
                    remote_vpc_id: str,
                    account_id: str,
                    region: str,
                    filters: list = []) -> dict:
    """
    Returns a dictionary describing a specific VPC peering between two VPCs
    From the perspective of a given account.
    Note: Same peering can have slightly different content based on perspective account.
    Note: This function is greedy in the sense that the requester does not have to be
    the requester as AWS defines it in https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html
    This function will match peerings even when the remote_vpc_id is the requester.

    Example usage:
    ```
    filters = [{'Name': 'tag:peerd_created', 'Values': ['true']},
                   {'Name': 'tag:peerd_environment', 'Values': [metadata['environment']]},
                   {'Name': 'status-code', 'Values': ['active']}]
        if not (peering := get_vpc_peering(vpc_id, remote_vpc_id, account_id, region, filters)):
            LOGGER.warning(f'No active peering between {vpc_id} and {remote_vpc_id} for this environment'
                           f' {metadata["environment"]}. It may exist as part of another environment.')
            continue
    ```


    :param vpc_id: A vpc id e.g. vpc-abc123
    :type vpc_id: str
    :param remote_vpc_id: A vpc id e.g. vpc-abc123
    :type remote_vpc_id: str
    :param account_id: An AWS account id number
    :type account: str
    :param region: An AWS region. e.g. us-east-1
    :type region: str
    :param filters: A standard list of boto3 filters (list of dics).
    :type filters: list
    :returns: A list containing all peering dictionaries for an account-region.
    :rtype: list
    """
    vpc_peerings = get_all_peerings(account_id, region, filters)
    for peering in vpc_peerings:
        if peering['AccepterVpcInfo']['VpcId'] in [vpc_id, remote_vpc_id]:
            if peering['RequesterVpcInfo']['VpcId'] in [vpc_id, remote_vpc_id]:
                LOGGER.debug(
                    f"Found peering {peering['VpcPeeringConnectionId']} between {vpc_id} and {remote_vpc_id}"
                )
                return peering
    LOGGER.debug(
        f"No active peering between {vpc_id} and {remote_vpc_id} for given filters {filters}"
    )
    return None
Exemple #7
0
def describe_account_vpcs_cached(account_id: str, region: str) -> List[dict]:
    """
    Describe all the VPCs in a given account in a given region and return them as a list of descriptions
    See https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.describe_vpcs

    :param account_id: An AWS account id number
    :type account: str
    :param region: An AWS region where the service client should be created. e.g. us-east-1
    :type region: str
    :returns: A list of dictionaries representing all the VPCs for a given account and region.
    :rtype: list
    """
    LOGGER.info(f'Fetching description of all VPCs for account {account_id} in region {region}')
    ec2_client = aws_client(account_id, 'ec2', region)
    return ec2_client.describe_vpcs()['Vpcs']
Exemple #8
0
def check_iam_role_capability(account_id: str) -> bool:
    """
    Check that we can assume a role in each account and perform an ec2 action to validate it.
    Allows us to filter out accounts which do not work for the script and inform the user.

    :param account_id: An AWS Account Id
    :type account_id: str
    :returns: True if the role and EC2 client works. False otherwise.
    :rtype: bool
    """
    try:
        if ec2_client := aws_client(account_id, 'ec2', 'us-east-1'):
            ec2_client.describe_vpcs()
            LOGGER.info(f'Able to assume role in {account_id} and access the EC2 API.')
            return True
        LOGGER.warning(f'Unable to assume role in {account_id} and access the EC2 API.')
Exemple #9
0
def tag_resource(client, resource: str, tags: dict, dryrun: bool = False):
    """
    Tags an AWS resource with the provided tags.

    Example Usage:
    ```
    ec2_client = aws_client(account_id, 'ec2', region)
    tags = {'peerd_support': '*****@*****.**',
            'peerd_datetime': str(datetime.now())}
    tag_resource(ec2_client, peerding_id, tags)
    ```
    :param client: An AWS boto3 client connection to an AWS resource
    :type client: boto3 client
    :param resource: The name of the resource e.g. rt-abc123
    :type resource: string
    :param tags: Dictionary of tags to apply to a resource
    :type tags: dict
    :returns: Nothing
    :raises BaseException: Raises an exception if there was some problem tagging the resource
    """
    tags = [{'Key': key, 'Value': value} for (key, value) in tags.items()]

    for x in range(5):
        try:
            LOGGER.debug(f'Tagging {resource}')
            client.create_tags(Resources=[resource], Tags=tags, DryRun=dryrun)
            LOGGER.debug(f'Tagging {resource} successful')
            return
        except botocore.exceptions.ClientError as err:
            if err.response['Error']['Code'] == 'DryRunOperation':
                LOGGER.debug(f'Tagging {resource} successful')
                return
            LOGGER.info(
                f'Tagging {resource} encountered error: {err.response["Error"]["Message"]}. Will retry.'
            )
            sleep(1)
            continue
        except BaseException:
            LOGGER.warning("Unexpected error: %s",
                           sys.exc_info()[1],
                           exc_info=True)

    raise Exception(f'Could not tag resource {resource}')
Exemple #10
0
def get_deletable_peerings(peerings: list, vpc_list: list) -> list:
    """
    Given a list of peerings that exist in the infrastructure, and a
    list of vpcs that are configured to be members of this environment,
    returns a list of peerings which should be deleted.

    :param config: A list containing dictionaries representing the configuration for the environment
    :type config: list
    :param key: The key to deduplicate on.
    :returns: A deduplicated list of all peerings for this environment.
    :rtype: list
    """
    peerings_to_delete = []
    for peering in peerings:
        accepter = peering['AccepterVpcInfo']
        requester = peering['RequesterVpcInfo']
        if (accepter['VpcId'] not in vpc_list) or (requester['VpcId'] not in vpc_list):
            LOGGER.debug(f"Peering between {requester['VpcId']} and {accepter['VpcId']} can be deleted.")
            peerings_to_delete.append(peering)
    return deduplicate_list_dicts(peerings_to_delete, 'VpcPeeringConnectionId')
Exemple #11
0
def clean_route_tables(peering_id: str, vpc_id: str, account_id: str, region: str, dryrun: bool) -> None:
    """
    Deletes any routes pointing at a given peering id for a given vpc.
    Only applies to route tables with tag peerd_eligible:true

    :param peering_id: A vpc peering id e.g. pcx-011a291e5affc8d95
    :type peering_id: str
    :param vpc_id: A vpc id e.g. vpc-abc123
    :type vpc_id: str
    :param account_id: An AWS account id number
    :type account: str
    :param region: An AWS region where the service client should be created. e.g. us-east-1
    :type region: str
    """
    # Initialise EC2 client
    ec2_client = aws_client(account_id, 'ec2', region)

    # Get the route tables for the VPC
    if not (route_tables := vpc_route_tables(vpc_id, account_id, region)):
        LOGGER.warning(f'No peerd_eligible route tables in VPC {vpc_id}')
        return
Exemple #12
0
def get_role_credentials(account: str, sts_client: Any) -> dict:
    """
    Assumes a role and returns credentials for said role.
    Requires the COMMON_PRINCIPAL_NAME to be set, usually from metadata.

    Example returned dictionary:
    ```
    {
        "Expiration": "2020-01-27T11:55:44Z",
        "Token": "abc123",
        "SecretAccessKey": "def456",
        "AccessKeyId": "ABCDEF123",
        "Type": "AWS-SOMETHING",
        "LastUpdated": "2020-01-27T10:55:45Z",
        "Code": "Success"
    }

    :param account: An AWS account id number
    :type account: str
    :param sts_client: an aws client connection to the sts service
    :type resource: boto3 client
    :returns: Dictionary containing the credentials from the assume role action
    :rtype: dict
    """
    try:
        arn = f'arn:aws:iam::{account}:role/{COMMON_PRINCIPAL_NAME}'
        LOGGER.info(f'Attempting to tokenise into: {arn}')
        credentials = sts_client.assume_role(RoleArn=arn, RoleSessionName=ROLE_SESSION_NAME)['Credentials']
        LOGGER.info(f'Successfully tokenised into: {arn}')
        return credentials
    except BaseException:
        LOGGER.warning("Unexpected error: %s", sys.exc_info()[1], exc_info=True)
        return {}
Exemple #13
0
def accept_vpc_peerings(target_peerings: list, metadata: dict, dryrun: bool):
    """
    Loops through a list of peerings, with existing peering id, to accept them.
    Requests, accepts and tags them.
    Repairs any half open peerings.

    Example target_peerings:
    ```
    [
        [
        {
            "account_id": "415432961280",
            "peering_id": "pcx-41u5h345h2",
            "vpc_id": "vpc-e08fb484",
            "region": "ap-southeast-2",
            "cidr_overrides": [
                "10.53.101.0/27"
            ],
            "peering_tags": [
                {
                "peerd_az_affinity": "0"
                }
            ]
        },
        {
            "account_id": 415432961280,
            "peering_id": "pcx-41u5h345h2",
            "vpc_id": "vpc-7a83b81e",
            "region": "ap-southeast-2"
        }
        ]
    ]
    ```

    :param target_peerings: A list of lists representing the requester and accepter for each peering.
    :type target_peerings: list
    :param metadata: A dictionary with the environment, owner, etc for tagging
    :type metadata: list
    """
    for peering_descriptor in target_peerings:

        # Unpack some common variables
        account_id = peering_descriptor[0]['account_id']
        vpc_id = peering_descriptor[0]['vpc_id']
        region = peering_descriptor[0]['region']
        local_tags = peering_descriptor[0].get('peering_tags', {})

        remote_account_id = peering_descriptor[1]['account_id']
        remote_vpc_id = peering_descriptor[1]['vpc_id']
        remote_region = peering_descriptor[1]['region']
        remote_tags = peering_descriptor[1].get('peering_tags', {})
        peering_id = peering_descriptor[1]['peering_id']
        tags = peering_descriptor[1]['tags']

        try:
            # Accept the VPC Peering
            LOGGER.info(
                f"Accepting peering request {peering_id} between {account_id} {vpc_id} {region} and "
                f"{remote_account_id} {remote_vpc_id} {remote_region}")
            ec2_client = aws_client(remote_account_id, 'ec2', remote_region)
            # Wait until the peering exists
            # The AWS API is eventually consistent and we need to wait.
            LOGGER.info(f'Waiting for peering to exist...')
            ec2_client.get_waiter('vpc_peering_connection_exists').wait(
                VpcPeeringConnectionIds=[peering_id],
                WaiterConfig={'Delay': 5})
            # Tag the VPC Peering
            tags['Name'] = f'peerd peering to {account_id} {vpc_id} {region}'
            tags['peerd_role'] = 'accepter'
            tag_resource(ec2_client, peering_id, tags, dryrun=dryrun)
            # Accept the peering
            try:
                ec2_client.accept_vpc_peering_connection(
                    VpcPeeringConnectionId=peering_id, DryRun=dryrun)
            except ClientError as err:
                if err.response['Error']['Code'] == 'DryRunOperation':
                    continue
                raise
        except BaseException:
            LOGGER.error("Unexpected error: %s",
                         sys.exc_info()[1],
                         exc_info=True)
            continue

        LOGGER.info(
            f"Successfully accepted peering {peering_id} between {account_id} {vpc_id} "
            f"{region} and {remote_account_id} {remote_vpc_id} {remote_region}"
        )
Exemple #14
0
         f" and {remote_account_id} {remote_vpc_id} {remote_region}"
     )
     continue
 # If the peering is pending acceptance move to tagging and acceptance
 # Only the remote account can accept the VPC peering.
 elif peering['Status']['Code'] == 'pending-acceptance' and peering[
         'RequesterVpcInfo']['VpcId'] == vpc_id:
     peering_id = peering['VpcPeeringConnectionId']
     LOGGER.warning(
         f"Pending-Acceptance peering {peering_id} between {account_id} {vpc_id} {region}"
         f" and {remote_account_id} {remote_vpc_id} {remote_region}. Will attempt recovery."
     )
 # We're in some weird state and need to report to a human
 else:
     LOGGER.warning(
         f"Peering between {account_id} {vpc_id} {region}"
         f" and {remote_account_id} {remote_vpc_id} {remote_region}"
         f" is in state {peering['Status']['Code']}.")
     continue
 # Tag the VPC Peering
 tags = {
     'Name':
     f'peerd peering to {remote_account_id} {remote_vpc_id} {remote_region}',
     'resource_owner': metadata['resource_owner'],
     'business_unit': metadata['business_unit'],
     'service_name': metadata['service_name'],
     'peerd_created': 'true',
     'peerd_support': metadata['support'],
     'peerd_datetime': str(datetime.now()),
     'peerd_role': 'requester',
     'peerd_environment': metadata['environment']
 }
Exemple #15
0
def create_vpc_peerings(target_peerings: Sequence, metadata: Mapping,
                        dryrun: bool) -> None:
    """
    Loops through a list of peerings to create them.
    Requests, accepts and tags them.
    Repairs any half open peerings.

    Example target_peerings:
    ```
    [
        [
        {
            "account_id": 415432961280,
            "vpc_id": "vpc-e08fb484",
            "region": "ap-southeast-2",
            "cidr_overrides": [
                "10.53.101.0/27"
            ],
            "peering_tags": [
                {
                "peerd_az_affinity": "0"
                }
            ]
        },
        {
            "account_id": 415432961280,
            "vpc_id": "vpc-7a83b81e",
            "region": "ap-southeast-2"
        }
        ]
    ]
    ```

    :param target_peerings: A list of lists representing the requester and accepter for each peering.
    :type target_peerings: list
    :param metadata: A dictionary with the environment, owner, etc for tagging
    :type metadata: list
    """
    pending_acceptance_peerings = []
    for peering_descriptor in target_peerings:

        # Unpack some common variables
        account_id = peering_descriptor[0]['account_id']
        vpc_id = peering_descriptor[0]['vpc_id']
        region = peering_descriptor[0]['region']
        local_tags = peering_descriptor[0].get('peering_tags', {})

        remote_account_id = peering_descriptor[1]['account_id']
        remote_vpc_id = peering_descriptor[1]['vpc_id']
        remote_region = peering_descriptor[1]['region']
        remote_tags = peering_descriptor[1].get('peering_tags', {})

        # Create a VPC peering request
        try:
            ec2_client = aws_client(account_id, 'ec2', region)
            # If the peering doesn't exist, create it
            if not (peering := get_vpc_peering(vpc_id, remote_vpc_id,
                                               account_id, region)):
                LOGGER.info(
                    f"Creating peering request between {account_id} {vpc_id} {region}"
                    f" and {remote_account_id} {remote_vpc_id} {remote_region}"
                )
                try:
                    ec2_peering_response = ec2_client.create_vpc_peering_connection(
                        VpcId=vpc_id,
                        PeerVpcId=remote_vpc_id,
                        PeerOwnerId=remote_account_id,
                        PeerRegion=remote_region,
                        DryRun=dryrun)['VpcPeeringConnection']
                except ClientError as err:
                    if err.response['Error']['Code'] == 'DryRunOperation':
                        continue
                    raise
                peering_id = ec2_peering_response['VpcPeeringConnectionId']
                # Wait for the vpc peering to exist before moving on
                LOGGER.info(f'Waiting for peering {peering_id} to exist...')
                ec2_client.get_waiter('vpc_peering_connection_exists').wait(
                    VpcPeeringConnectionIds=[peering_id],
                    WaiterConfig={'Delay': 5})
            # If the peering exists and is active, do nothing.
            elif peering['Status']['Code'] == 'active':
                LOGGER.info(
                    f"Active peering {peering['VpcPeeringConnectionId']} between {account_id} {vpc_id} {region}"
                    f" and {remote_account_id} {remote_vpc_id} {remote_region}"
                )
                continue
            # If the peering is pending acceptance move to tagging and acceptance
            # Only the remote account can accept the VPC peering.
            elif peering['Status']['Code'] == 'pending-acceptance' and peering[
                    'RequesterVpcInfo']['VpcId'] == vpc_id:
                peering_id = peering['VpcPeeringConnectionId']
                LOGGER.warning(
                    f"Pending-Acceptance peering {peering_id} between {account_id} {vpc_id} {region}"
                    f" and {remote_account_id} {remote_vpc_id} {remote_region}. Will attempt recovery."
                )
Exemple #16
0
    # If we can't we return none and cache none.
    if not (credentials := get_role_credentials(account, aws_sts_client())):
        LOGGER.warning(f'Unable to tokenise into {account} for service {service} in region {region}. Moving on')
        return None

    # In this block we use the account credentials we got above, to create and cache a client
    # connection to an AWS service.
    try:
        client =  boto3.client(
            service,
            region_name=region,
            aws_access_key_id=credentials['AccessKeyId'],
            aws_secret_access_key=credentials['SecretAccessKey'],
            aws_session_token=credentials['SessionToken'],
            config=Config(retries=dict(max_attempts=10)))
        LOGGER.debug('Obtained fresh client connection.')
        return client
    except BaseException:
        LOGGER.error(f'Unexpected error: {sys.exc_info()[1]}', exc_info=True)


def tag_resource(client: Any, resource: str, tags: Mapping, dryrun: bool = False) -> None:
    """
    Tags an AWS resource with the provided tags.

    Example Usage:
    ```
    ec2_client = aws_client(account_id, 'ec2', region)
    tags = {'peerd_support': '*****@*****.**',
            'peerd_datetime': str(datetime.now())}
    tag_resource(ec2_client, peerding_id, tags)
Exemple #17
0
def update_route_tables(target_peerings: list, metadata: Mapping,
                        dryrun: bool) -> None:
    """
    Loops through a list of peerings and updates the route tables on each side.

    Example target_peerings:
    ```
    [
        [
        {
            "account_id": 415432961280,
            "vpc_id": "vpc-e08fb484",
            "region": "ap-southeast-2",
            "cidr_overrides": [
                "10.53.101.0/27"
            ],
            "peering_tags": [
                {
                "peerd_az_affinity": "0"
                }
            ]
        },
        {
            "account_id": 415432961280,
            "vpc_id": "vpc-7a83b81e",
            "region": "ap-southeast-2"
        }
        ]
    ]
    ```

    :param target_peerings: A list of lists representing the requester and accepter for each peering.
    :type target_peerings: list
    :param metadata: A dictionary with the environment, owner, etc for tagging
    :type metadata: list
    """

    # We need to handle both sides of the peerings so we append reverse of each peering.
    # This means every side of every peering will be seen by a single loop.
    # We do this to avoid extra code handling the AWS concept of "accepter" and "requester"
    # We also construct our route table cache by account and region, which means if we looped
    # by requester and accepter we could cache stale route table contents.
    target_peerings.extend([x[::-1] for x in target_peerings])

    # Loop through the target peerings
    for peering_descriptor in target_peerings:
        # Unpack some common variables
        account_id = peering_descriptor[0]['account_id']
        vpc_id = peering_descriptor[0]['vpc_id']
        region = peering_descriptor[0]['region']

        remote_account_id = peering_descriptor[1]['account_id']
        remote_vpc_id = peering_descriptor[1]['vpc_id']
        remote_region = peering_descriptor[1]['region']
        # Get the remote CIDRs from the AWS VPC API, or use the overrides in the config if they exist.
        remote_cidrs = peering_descriptor[1].get(
            'cidr_overrides',
            list_vpc_cidrs(remote_vpc_id, remote_account_id, remote_region))

        LOGGER.info(
            f"Inspecting route tables in {account_id} {vpc_id} "
            f"{region}, peer: {remote_account_id} {remote_vpc_id} {remote_region}"
        )

        # Initialise a ec2 client connection
        ec2_client = aws_client(account_id, 'ec2', region)

        # Get active VPC peering if one exists, else continue
        # We want to avoid adding routes for inactive peerings.
        filters = [{
            'Name': 'tag:peerd_created',
            'Values': ['true']
        }, {
            'Name': 'tag:peerd_environment',
            'Values': [metadata['environment']]
        }, {
            'Name': 'status-code',
            'Values': ['active', 'provisioning']
        }]
        if not (peering := get_vpc_peering(vpc_id, remote_vpc_id, account_id,
                                           region, filters)):
            # Since we filter for the peerd environment, remind the user that there could be a peering
            # But that it might exist as part of another environment, and thus we won't be touching it.
            LOGGER.warning(
                f'No active peering between {vpc_id} and {remote_vpc_id} for this environment'
                f' {metadata["environment"]}. It may exist as part of another environment.'
            )
            continue
        peering_id = peering['VpcPeeringConnectionId']

        # Wait until the peering is active, not provisioning (boto waiter doesn't accept filters for vpc peering api)
        # We must wait for active state to install routes, otherwise the peering will be ignored.
        # Note, usually this step takes a few seconds, but can sometimes take up to a minute or two in rare cases.
        if peering['Status']['Code'] == 'provisioning':
            while not ec2_client.describe_vpc_peering_connections(
                    Filters=[{
                        'Name': 'status-code',
                        'Values': ['active']
                    }, {
                        'Name': 'vpc-peering-connection-id',
                        'Values': [peering_id]
                    }])['VpcPeeringConnections']:
                LOGGER.info(
                    f'Waiting for peering {peering_id} to become active...')
                sleep(5)

        # Get the route tables for the local vpc relevant to the peering.
        # The vpc_route_tables function will only return peerd_eligible:true tables
        # Since we will have cases where RTs should not be altered.
        if not (route_tables := vpc_route_tables(vpc_id, account_id, region)):
            LOGGER.warning(f'No peerd_eligible route tables in VPC {vpc_id}')
            continue
Exemple #18
0
def delete_unneeded_peerings(config: Sequence[dict], metadata: Mapping,
                             dryrun: bool) -> None:
    """
    Compares the infrastructure with the configuration and applies
    route cleanup and peering deletion logic to remove VPC peerings.

    :param peering_id: A vpc peering id e.g. pcx-011a291e5affc8d95
    :type peering_id: str
    :param vpc_id: A vpc id e.g. vpc-abc123
    :type vpc_id: str
    :param account_id: An AWS account id number
    :type account: str
    :param region: An AWS region where the service client should be created. e.g. us-east-1
    :type region: str
    """
    LOGGER.info('Beginning deletion phase...')
    # Get a list of all VPCs configured for this environment
    config_vpc_list = list_dict_values(config, 'vpc_id')
    # Get all peerings that exist for this environment
    LOGGER.info(
        f'Getting all peerings active in AWS for environment {metadata["environment"]}'
    )
    # Only filter working accounts now, as there could be times where accounts don't work
    # but we might want to keep the peerings until we remove them from the configuration.
    filtered_config = filter_working_accounts(config)
    peerings = get_all_env_peerings(filtered_config, metadata)
    # Determine which peerings no longer relate to any vpc in the environment
    LOGGER.info(
        f'Calculating which peerings may be deleted (do not appear in configuration)'
    )
    deletable_peerings = get_deletable_peerings(peerings, config_vpc_list)
    # Iterate through the deletable peerings
    for peering in deletable_peerings:
        peering_id = peering['VpcPeeringConnectionId']
        LOGGER.info(
            'Working on peering {} between {} {} {} and {} {} {}'.format(
                peering_id,
                peering['RequesterVpcInfo']['OwnerId'],
                peering['RequesterVpcInfo']['VpcId'],
                peering['RequesterVpcInfo']['Region'],
                peering['AccepterVpcInfo']['OwnerId'],
                peering['AccepterVpcInfo']['VpcId'],
                peering['AccepterVpcInfo']['Region'],
            ))

        # Clean up the route tables on both sides
        for vpc_info in [
                peering['RequesterVpcInfo'], peering['AccepterVpcInfo']
        ]:
            clean_route_tables(peering_id, vpc_info['VpcId'],
                               vpc_info['OwnerId'], vpc_info['Region'], dryrun)
        # Delete the peering
        LOGGER.info(f"Deleting peering {peering_id}...")
        ec2_client = aws_client(peering['RequesterVpcInfo']['OwnerId'], 'ec2',
                                peering['RequesterVpcInfo']['Region'])
        try:
            ec2_client.delete_vpc_peering_connection(
                VpcPeeringConnectionId=peering_id, DryRun=dryrun)
        except ClientError as err:
            if err.response['Error']['Code'] != 'DryRunOperation':
                raise

        LOGGER.info(f"Deleted peering {peering_id} successfully.")
Exemple #19
0
         f" and {remote_account_id} {remote_vpc_id} {remote_region}"
     )
     continue
 # If the peering is pending acceptance move to tagging and acceptance
 # Only the remote account can accept the VPC peering.
 elif peering['Status']['Code'] == 'pending-acceptance' and peering[
         'RequesterVpcInfo']['VpcId'] == vpc_id:
     peerding_id = peering['VpcPeeringConnectionId']
     LOGGER.warning(
         f"Pending peering between {account_id} {vpc_id} {region}"
         f" and {remote_account_id} {remote_vpc_id} {remote_region}. Will attempt recovery."
     )
 # We're in some weird state and need to report to a human
 else:
     LOGGER.warning(
         f"Peering between {account_id} {vpc_id} {region}"
         f" and {remote_account_id} {remote_vpc_id} {remote_region}"
         f" is in state {peering['Status']['Code']}.")
     continue
 # Tag the VPC Peering
 tags = {
     'Name':
     f'peerd peering to {remote_account_id} {remote_vpc_id} {remote_region}',
     'resource_owner': metadata['resource_owner'],
     'business_unit': metadata['business_unit'],
     'service_name': metadata['service_name'],
     'peerd_created': 'true',
     'peerd_support': metadata['support'],
     'peerd_datetime': str(datetime.now()),
     'peerd_role': 'requester',
     'peerd_environment': metadata['environment']
 }