Skip to content

Commit

Permalink
EC2: Cross-account VPC peering connections (getmoto#6826)
Browse files Browse the repository at this point in the history
  • Loading branch information
viren-nadkarni authored and toshyak committed Oct 26, 2023
1 parent 2b8e8ad commit 247d553
Show file tree
Hide file tree
Showing 6 changed files with 635 additions and 256 deletions.
19 changes: 15 additions & 4 deletions moto/ec2/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,21 +632,23 @@ def __init__(self, cidr_block: str):
super().__init__("InvalidVpc.Range", f"The CIDR '{cidr_block}' is invalid.")


# accept exception
# Raised when attempting to accept a VPC peering connection request in own account but in the requester region
class OperationNotPermitted2(EC2ClientError):
def __init__(self, client_region: str, pcx_id: str, acceptor_region: str):
super().__init__(
"OperationNotPermitted",
f"Incorrect region ({client_region}) specified for this request.VPC peering connection {pcx_id} must be accepted in region {acceptor_region}",
f"Incorrect region ({client_region}) specified for this request. "
f"VPC peering connection {pcx_id} must be accepted in region {acceptor_region}",
)


# reject exception
# Raised when attempting to reject a VPC peering connection request in own account but in the requester region
class OperationNotPermitted3(EC2ClientError):
def __init__(self, client_region: str, pcx_id: str, acceptor_region: str):
super().__init__(
"OperationNotPermitted",
f"Incorrect region ({client_region}) specified for this request.VPC peering connection {pcx_id} must be accepted or rejected in region {acceptor_region}",
f"Incorrect region ({client_region}) specified for this request. "
f"VPC peering connection {pcx_id} must be accepted or rejected in region {acceptor_region}",
)


Expand All @@ -658,6 +660,15 @@ def __init__(self, instance_id: str):
)


# Raised when attempting to accept or reject a VPC peering connection request for a VPC not belonging to self
class OperationNotPermitted5(EC2ClientError):
def __init__(self, account_id: str, pcx_id: str, operation: str):
super().__init__(
"OperationNotPermitted",
f"User ({account_id}) cannot {operation} peering {pcx_id}",
)


class InvalidLaunchTemplateNameAlreadyExistsError(EC2ClientError):
def __init__(self) -> None:
super().__init__(
Expand Down
4 changes: 2 additions & 2 deletions moto/ec2/models/transit_gateway_attachments.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def __init__(
"region": region_name,
"transitGatewayId": transit_gateway_id,
}
self.status = PeeringConnectionStatus()
self.status = PeeringConnectionStatus(accepter_id=peer_account_id)


class TransitGatewayAttachmentBackend:
Expand Down Expand Up @@ -342,5 +342,5 @@ def delete_transit_gateway_peering_attachment(
transit_gateway_attachment_id
]
transit_gateway_attachment.state = "deleted"
transit_gateway_attachment.status.deleted() # type: ignore[attr-defined]
transit_gateway_attachment.status.deleted(deleter_id=self.account_id) # type: ignore[attr-defined]
return transit_gateway_attachment
62 changes: 43 additions & 19 deletions moto/ec2/models/vpc_peering_connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,32 @@
InvalidVPCPeeringConnectionStateTransitionError,
OperationNotPermitted2,
OperationNotPermitted3,
OperationNotPermitted5,
)
from .core import TaggedEC2Resource
from .vpcs import VPC
from ..utils import random_vpc_peering_connection_id


class PeeringConnectionStatus:
def __init__(self, code: str = "initiating-request", message: str = ""):
def __init__(
self, accepter_id: str, code: str = "initiating-request", message: str = ""
):
self.accepter_id = accepter_id
self.code = code
self.message = message

def deleted(self) -> None:
def deleted(self, deleter_id: str) -> None:
self.code = "deleted"
self.message = "Deleted by {deleter ID}"
self.message = f"Deleted by {deleter_id}"

def initiating(self) -> None:
self.code = "initiating-request"
self.message = "Initiating Request to {accepter ID}"
self.message = f"Initiating Request to {self.accepter_id}"

def pending(self) -> None:
self.code = "pending-acceptance"
self.message = "Pending Acceptance by {accepter ID}"
self.message = f"Pending Acceptance by {self.accepter_id}"

def accept(self) -> None:
self.code = "active"
Expand Down Expand Up @@ -61,7 +65,7 @@ def __init__(
self.requester_options = self.DEFAULT_OPTIONS.copy()
self.accepter_options = self.DEFAULT_OPTIONS.copy()
self.add_tags(tags or {})
self._status = PeeringConnectionStatus()
self._status = PeeringConnectionStatus(accepter_id=peer_vpc.owner_id)

@staticmethod
def cloudformation_name_type() -> str:
Expand All @@ -79,7 +83,7 @@ def create_from_cloudformation_json( # type: ignore[misc]
cloudformation_json: Any,
account_id: str,
region_name: str,
**kwargs: Any
**kwargs: Any,
) -> "VPCPeeringConnection":
from ..models import ec2_backends

Expand Down Expand Up @@ -120,11 +124,15 @@ def create_vpc_peering_connection(
vpc_pcx = VPCPeeringConnection(self, vpc_pcx_id, vpc, peer_vpc, tags)
vpc_pcx._status.pending()
self.vpc_pcxs[vpc_pcx_id] = vpc_pcx
# insert cross region peering info
if vpc.ec2_backend.region_name != peer_vpc.ec2_backend.region_name:
for vpc_pcx_cx in peer_vpc.ec2_backend.get_vpc_pcx_refs():
if vpc_pcx_cx.region_name == peer_vpc.ec2_backend.region_name:
vpc_pcx_cx.vpc_pcxs[vpc_pcx_id] = vpc_pcx
# insert cross-account/cross-region peering info
if vpc.owner_id != peer_vpc.owner_id or vpc.region != peer_vpc.region:
for backend in peer_vpc.ec2_backend.get_vpc_pcx_refs():
if (
backend.account_id == peer_vpc.owner_id
and backend.region_name == peer_vpc.region
):
backend.vpc_pcxs[vpc_pcx_id] = vpc_pcx

return vpc_pcx

def describe_vpc_peering_connections(
Expand All @@ -142,28 +150,44 @@ def get_vpc_peering_connection(self, vpc_pcx_id: str) -> VPCPeeringConnection:

def delete_vpc_peering_connection(self, vpc_pcx_id: str) -> VPCPeeringConnection:
deleted = self.get_vpc_peering_connection(vpc_pcx_id)
deleted._status.deleted()
deleted._status.deleted(deleter_id=self.account_id) # type: ignore[attr-defined]
return deleted

def accept_vpc_peering_connection(self, vpc_pcx_id: str) -> VPCPeeringConnection:
vpc_pcx = self.get_vpc_peering_connection(vpc_pcx_id)
# if cross region need accepter from another region
pcx_req_region = vpc_pcx.vpc.ec2_backend.region_name
pcx_acp_region = vpc_pcx.peer_vpc.ec2_backend.region_name

# validate cross-account acceptance
req_account_id = vpc_pcx.vpc.owner_id
acp_account_id = vpc_pcx.peer_vpc.owner_id
if req_account_id != acp_account_id and self.account_id != acp_account_id: # type: ignore[attr-defined]
raise OperationNotPermitted5(self.account_id, vpc_pcx_id, "accept") # type: ignore[attr-defined]

# validate cross-region acceptance
pcx_req_region = vpc_pcx.vpc.region
pcx_acp_region = vpc_pcx.peer_vpc.region
if pcx_req_region != pcx_acp_region and self.region_name == pcx_req_region: # type: ignore[attr-defined]
raise OperationNotPermitted2(self.region_name, vpc_pcx.id, pcx_acp_region) # type: ignore[attr-defined]

if vpc_pcx._status.code != "pending-acceptance":
raise InvalidVPCPeeringConnectionStateTransitionError(vpc_pcx.id)
vpc_pcx._status.accept()
return vpc_pcx

def reject_vpc_peering_connection(self, vpc_pcx_id: str) -> VPCPeeringConnection:
vpc_pcx = self.get_vpc_peering_connection(vpc_pcx_id)
# if cross region need accepter from another region
pcx_req_region = vpc_pcx.vpc.ec2_backend.region_name
pcx_acp_region = vpc_pcx.peer_vpc.ec2_backend.region_name

# validate cross-account rejection
req_account_id = vpc_pcx.vpc.owner_id
acp_account_id = vpc_pcx.peer_vpc.owner_id
if req_account_id != acp_account_id and self.account_id != acp_account_id: # type: ignore[attr-defined]
raise OperationNotPermitted5(self.account_id, vpc_pcx_id, "reject") # type: ignore[attr-defined]

# validate cross-region acceptance
pcx_req_region = vpc_pcx.vpc.region
pcx_acp_region = vpc_pcx.peer_vpc.region
if pcx_req_region != pcx_acp_region and self.region_name == pcx_req_region: # type: ignore[attr-defined]
raise OperationNotPermitted3(self.region_name, vpc_pcx.id, pcx_acp_region) # type: ignore[attr-defined]

if vpc_pcx._status.code != "pending-acceptance":
raise InvalidVPCPeeringConnectionStateTransitionError(vpc_pcx.id)
vpc_pcx._status.reject()
Expand Down
4 changes: 4 additions & 0 deletions moto/ec2/models/vpcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@ def __init__(
def owner_id(self) -> str:
return self.ec2_backend.account_id

@property
def region(self) -> str:
return self.ec2_backend.region_name

@staticmethod
def cloudformation_name_type() -> str:
return ""
Expand Down
114 changes: 68 additions & 46 deletions moto/ec2/responses/vpc_peering_connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@

class VPCPeeringConnections(EC2BaseResponse):
def create_vpc_peering_connection(self) -> str:
peer_region = self._get_param("PeerRegion")
tags = self._parse_tag_specification().get("vpc-peering-connection", {})

if peer_region == self.region or peer_region is None:
peer_vpc = self.ec2_backend.get_vpc(self._get_param("PeerVpcId"))
else:
from moto.ec2.models import ec2_backends
account_id = self._get_param("PeerOwnerId") or self.current_account
region_name = self._get_param("PeerRegion") or self.region

peer_vpc = ec2_backends[self.current_account][peer_region].get_vpc(
self._get_param("PeerVpcId")
)
vpc = self.ec2_backend.get_vpc(self._get_param("VpcId"))

# Peer VPC could belong to another account or region
from moto.ec2.models import ec2_backends

peer_vpc = ec2_backends[account_id][region_name].get_vpc(
self._get_param("PeerVpcId")
)

vpc_pcx = self.ec2_backend.create_vpc_peering_connection(vpc, peer_vpc, tags)
template = self.response_template(CREATE_VPC_PEERING_CONNECTION_RESPONSE)
return template.render(account_id=self.current_account, vpc_pcx=vpc_pcx)
return template.render(vpc_pcx=vpc_pcx)

def delete_vpc_peering_connection(self) -> str:
vpc_pcx_id = self._get_param("VpcPeeringConnectionId")
Expand All @@ -31,13 +33,13 @@ def describe_vpc_peering_connections(self) -> str:
vpc_peering_ids=ids
)
template = self.response_template(DESCRIBE_VPC_PEERING_CONNECTIONS_RESPONSE)
return template.render(account_id=self.current_account, vpc_pcxs=vpc_pcxs)
return template.render(vpc_pcxs=vpc_pcxs)

def accept_vpc_peering_connection(self) -> str:
vpc_pcx_id = self._get_param("VpcPeeringConnectionId")
vpc_pcx = self.ec2_backend.accept_vpc_peering_connection(vpc_pcx_id)
template = self.response_template(ACCEPT_VPC_PEERING_CONNECTION_RESPONSE)
return template.render(account_id=self.current_account, vpc_pcx=vpc_pcx)
return template.render(vpc_pcx=vpc_pcx)

def reject_vpc_peering_connection(self) -> str:
vpc_pcx_id = self._get_param("VpcPeeringConnectionId")
Expand Down Expand Up @@ -68,39 +70,46 @@ def modify_vpc_peering_connection_options(self) -> str:
<CreateVpcPeeringConnectionResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
<requestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</requestId>
<vpcPeeringConnection>
<vpcPeeringConnectionId>{{ vpc_pcx.id }}</vpcPeeringConnectionId>
<requesterVpcInfo>
<ownerId>{{ account_id }}</ownerId>
<vpcPeeringConnectionId>{{ vpc_pcx.id }}</vpcPeeringConnectionId>
<requesterVpcInfo>
<ownerId>{{ vpc_pcx.vpc.owner_id }}</ownerId>
<region>{{ vpc_pcx.vpc.region }}</region>
<vpcId>{{ vpc_pcx.vpc.id }}</vpcId>
<cidrBlock>{{ vpc_pcx.vpc.cidr_block }}</cidrBlock>
<cidrBlockSet></cidrBlockSet>
<ipv6CidrBlockSet></ipv6CidrBlockSet>
<peeringOptions>
<allowEgressFromLocalClassicLinkToRemoteVpc>{{ vpc_pcx.requester_options.AllowEgressFromLocalClassicLinkToRemoteVpc or '' }}</allowEgressFromLocalClassicLinkToRemoteVpc>
<allowEgressFromLocalVpcToRemoteClassicLink>{{ vpc_pcx.requester_options.AllowEgressFromLocalVpcToRemoteClassicLink or '' }}</allowEgressFromLocalVpcToRemoteClassicLink>
<allowDnsResolutionFromRemoteVpc>{{ vpc_pcx.requester_options.AllowDnsResolutionFromRemoteVpc or '' }}</allowDnsResolutionFromRemoteVpc>
</peeringOptions>
</requesterVpcInfo>
<accepterVpcInfo>
<ownerId>{{ account_id }}</ownerId>
<vpcId>{{ vpc_pcx.peer_vpc.id }}</vpcId>
<peeringOptions>
<allowEgressFromLocalClassicLinkToRemoteVpc>{{ vpc_pcx.accepter_options.AllowEgressFromLocalClassicLinkToRemoteVpc or '' }}</allowEgressFromLocalClassicLinkToRemoteVpc>
<allowEgressFromLocalVpcToRemoteClassicLink>{{ vpc_pcx.accepter_options.AllowEgressFromLocalVpcToRemoteClassicLink or '' }}</allowEgressFromLocalVpcToRemoteClassicLink>
<allowDnsResolutionFromRemoteVpc>{{ vpc_pcx.accepter_options.AllowDnsResolutionFromRemoteVpc or '' }}</allowDnsResolutionFromRemoteVpc>
</peeringOptions>
</accepterVpcInfo>
<status>
</requesterVpcInfo>
<accepterVpcInfo>
<ownerId>{{ vpc_pcx.peer_vpc.owner_id }}</ownerId>
<region>{{ vpc_pcx.peer_vpc.region }}</region>
<vpcId>{{ vpc_pcx.peer_vpc.id }}</vpcId>
<cidrBlock>{{ vpc_pcx.peer_vpc.cidr_block }}</cidrBlock>
<cidrBlockSet></cidrBlockSet>
<ipv6CidrBlockSet></ipv6CidrBlockSet>
<peeringOptions>
<allowEgressFromLocalClassicLinkToRemoteVpc>{{ vpc_pcx.accepter_options.AllowEgressFromLocalClassicLinkToRemoteVpc or '' }}</allowEgressFromLocalClassicLinkToRemoteVpc>
<allowEgressFromLocalVpcToRemoteClassicLink>{{ vpc_pcx.accepter_options.AllowEgressFromLocalVpcToRemoteClassicLink or '' }}</allowEgressFromLocalVpcToRemoteClassicLink>
<allowDnsResolutionFromRemoteVpc>{{ vpc_pcx.accepter_options.AllowDnsResolutionFromRemoteVpc or '' }}</allowDnsResolutionFromRemoteVpc>
</peeringOptions>
</accepterVpcInfo>
<status>
<code>initiating-request</code>
<message>Initiating Request to {accepter ID}</message>
</status>
<expirationTime>2014-02-18T14:37:25.000Z</expirationTime>
<tagSet>
{% for tag in vpc_pcx.get_tags() %}
<item>
<key>{{ tag.key }}</key>
<value>{{ tag.value }}</value>
</item>
{% endfor %}
</tagSet>
<message>Initiating Request to {{ vpc_pcx.peer_vpc.owner_id }}</message>
</status>
<expirationTime>2014-02-18T14:37:25.000Z</expirationTime>
<tagSet>
{% for tag in vpc_pcx.get_tags() %}
<item>
<key>{{ tag.key }}</key>
<value>{{ tag.value }}</value>
</item>
{% endfor %}
</tagSet>
</vpcPeeringConnection>
</CreateVpcPeeringConnectionResponse>
"""
Expand All @@ -113,21 +122,25 @@ def modify_vpc_peering_connection_options(self) -> str:
<item>
<vpcPeeringConnectionId>{{ vpc_pcx.id }}</vpcPeeringConnectionId>
<requesterVpcInfo>
<ownerId>{{ account_id }}</ownerId>
<ownerId>{{ vpc_pcx.vpc.owner_id }}</ownerId>
<region>{{ vpc_pcx.vpc.region }}</region>
<vpcId>{{ vpc_pcx.vpc.id }}</vpcId>
<cidrBlock>{{ vpc_pcx.vpc.cidr_block }}</cidrBlock>
<region>{{ vpc_pcx.vpc.ec2_backend.region_name }}</region>
<cidrBlockSet></cidrBlockSet>
<ipv6CidrBlockSet></ipv6CidrBlockSet>
<peeringOptions>
<allowEgressFromLocalClassicLinkToRemoteVpc>{{ vpc_pcx.requester_options.AllowEgressFromLocalClassicLinkToRemoteVpc or '' }}</allowEgressFromLocalClassicLinkToRemoteVpc>
<allowEgressFromLocalVpcToRemoteClassicLink>{{ vpc_pcx.requester_options.AllowEgressFromLocalVpcToRemoteClassicLink or '' }}</allowEgressFromLocalVpcToRemoteClassicLink>
<allowDnsResolutionFromRemoteVpc>{{ vpc_pcx.requester_options.AllowDnsResolutionFromRemoteVpc or '' }}</allowDnsResolutionFromRemoteVpc>
</peeringOptions>
</requesterVpcInfo>
<accepterVpcInfo>
<ownerId>{{ account_id }}</ownerId>
<ownerId>{{ vpc_pcx.peer_vpc.owner_id }}</ownerId>
<region>{{ vpc_pcx.peer_vpc.region }}</region>
<vpcId>{{ vpc_pcx.peer_vpc.id }}</vpcId>
<cidrBlock>{{ vpc_pcx.peer_vpc.cidr_block }}</cidrBlock>
<region>{{ vpc_pcx.peer_vpc.ec2_backend.region_name }}</region>
<cidrBlockSet></cidrBlockSet>
<ipv6CidrBlockSet></ipv6CidrBlockSet>
<peeringOptions>
<allowEgressFromLocalClassicLinkToRemoteVpc>{{ vpc_pcx.accepter_options.AllowEgressFromLocalClassicLinkToRemoteVpc or '' }}</allowEgressFromLocalClassicLinkToRemoteVpc>
<allowEgressFromLocalVpcToRemoteClassicLink>{{ vpc_pcx.accepter_options.AllowEgressFromLocalVpcToRemoteClassicLink or '' }}</allowEgressFromLocalVpcToRemoteClassicLink>
Expand Down Expand Up @@ -165,21 +178,30 @@ def modify_vpc_peering_connection_options(self) -> str:
<vpcPeeringConnection>
<vpcPeeringConnectionId>{{ vpc_pcx.id }}</vpcPeeringConnectionId>
<requesterVpcInfo>
<ownerId>{{ account_id }}</ownerId>
<vpcId>{{ vpc_pcx.vpc.id }}</vpcId>
<cidrBlock>{{ vpc_pcx.vpc.cidr_block }}</cidrBlock>
<region>{{ vpc_pcx.vpc.ec2_backend.region_name }}</region>
<ownerId>{{ vpc_pcx.vpc.owner_id }}</ownerId>
<region>{{ vpc_pcx.vpc.region }}</region>
<vpcId>{{ vpc_pcx.vpc.id }}</vpcId>
<cidrBlock>{{ vpc_pcx.vpc.cidr_block }}</cidrBlock>
<cidrBlockSet></cidrBlockSet>
<ipv6CidrBlockSet></ipv6CidrBlockSet>
<peeringOptions>
<allowEgressFromLocalClassicLinkToRemoteVpc>{{ vpc_pcx.requester_options.AllowEgressFromLocalClassicLinkToRemoteVpc or '' }}</allowEgressFromLocalClassicLinkToRemoteVpc>
<allowEgressFromLocalVpcToRemoteClassicLink>{{ vpc_pcx.requester_options.AllowEgressFromLocalVpcToRemoteClassicLink or '' }}</allowEgressFromLocalVpcToRemoteClassicLink>
<allowDnsResolutionFromRemoteVpc>{{ vpc_pcx.requester_options.AllowDnsResolutionFromRemoteVpc or '' }}</allowDnsResolutionFromRemoteVpc>
</peeringOptions>
</requesterVpcInfo>
<accepterVpcInfo>
<ownerId>{{ account_id }}</ownerId>
<ownerId>{{ vpc_pcx.peer_vpc.owner_id }}</ownerId>
<region>{{ vpc_pcx.peer_vpc.region }}</region>
<vpcId>{{ vpc_pcx.peer_vpc.id }}</vpcId>
<cidrBlock>{{ vpc_pcx.peer_vpc.cidr_block }}</cidrBlock>
<cidrBlockSet></cidrBlockSet>
<ipv6CidrBlockSet></ipv6CidrBlockSet>
<peeringOptions>
<allowEgressFromLocalClassicLinkToRemoteVpc>{{ vpc_pcx.accepter_options.AllowEgressFromLocalClassicLinkToRemoteVpc or '' }}</allowEgressFromLocalClassicLinkToRemoteVpc>
<allowEgressFromLocalVpcToRemoteClassicLink>{{ vpc_pcx.accepter_options.AllowEgressFromLocalVpcToRemoteClassicLink or '' }}</allowEgressFromLocalVpcToRemoteClassicLink>
<allowDnsResolutionFromRemoteVpc>{{ vpc_pcx.accepter_options.AllowDnsResolutionFromRemoteVpc or '' }}</allowDnsResolutionFromRemoteVpc>
</peeringOptions>
<region>{{ vpc_pcx.peer_vpc.ec2_backend.region_name }}</region>
</accepterVpcInfo>
<status>
<code>{{ vpc_pcx._status.code }}</code>
Expand Down

0 comments on commit 247d553

Please sign in to comment.