Repository: getmoto/moto

Issue:
EC2.ServiceResource.create_vpc_peering_connection does not create a peering connection on the peer VPC.
## Reproduce

### Test

```
# Pytest
import boto3
from moto import mock_ec2


def test():
    mock_ec2().start()

    ec2 = boto3.resource('ec2', region_name='us-east-1')
    peer_ec2 = boto3.resource('ec2', region_name='eu-west-1')
    vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16')
    peer_vpc = peer_ec2.create_vpc(CidrBlock='10.0.1.0/16')
    ec2.create_vpc_peering_connection(
        PeerRegion='eu-west-1',
        PeerVpcId=peer_vpc.id,
        VpcId=vpc.id
    )
    assert len(list(peer_ec2.vpc_peering_connections.all())) == 1
    mock_ec2().stop()
```

### Output:

FAILED                            [ 0%]
test.py:13 (test)
0 != 1

Expected :1
Actual   :0

## Expected

One peering connection on the peer VPC.

## Actual

No peering connections on the peer VPC.

## Library versions

```
moto = { git = "git://github.com/spulec/moto.git", ref = "dfa7935e13e6065810d6fc4e1edbf3e86499aea5" }
boto3 = "==1.7.79"
botocore = "==1.10.79"
```


Discussion:
Just to add this is specifically an issue for cross-region peering connections.
Thanks; I don't believe we've added support for this yet. Setting as a feature request.

Resolving patch:
diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py
--- a/moto/ec2/exceptions.py
+++ b/moto/ec2/exceptions.py
@@ -420,3 +420,25 @@ def __init__(self, association_id):
             "The vpc CIDR block with association ID {} may not be disassociated. "
             "It is the primary IPv4 CIDR block of the VPC".format(association_id)
         )
+
+
+# accept exception
+class OperationNotPermitted2(EC2ClientError):
+    def __init__(self, client_region, pcx_id, acceptor_region):
+        super(OperationNotPermitted2, self).__init__(
+            "OperationNotPermitted",
+            "Incorrect region ({0}) specified for this request."
+            "VPC peering connection {1} must be accepted in region {2}".format(client_region, pcx_id, acceptor_region)
+        )
+
+
+# reject exception
+class OperationNotPermitted3(EC2ClientError):
+    def __init__(self, client_region, pcx_id, acceptor_region):
+        super(OperationNotPermitted3, self).__init__(
+            "OperationNotPermitted",
+            "Incorrect region ({0}) specified for this request."
+            "VPC peering connection {1} must be accepted or rejected in region {2}".format(client_region,
+                                                                                           pcx_id,
+                                                                                           acceptor_region)
+        )
diff --git a/moto/ec2/models.py b/moto/ec2/models.py
--- a/moto/ec2/models.py
+++ b/moto/ec2/models.py
@@ -70,6 +70,8 @@
     MissingParameterError,
     MotoNotImplementedError,
     OperationNotPermitted,
+    OperationNotPermitted2,
+    OperationNotPermitted3,
     ResourceAlreadyAssociatedError,
     RulesPerSecurityGroupLimitExceededError,
     TagLimitExceeded)
@@ -2120,16 +2122,16 @@ def get_cidr_block_association_set(self, ipv6=False):
 
 
 class VPCBackend(object):
-    __refs__ = defaultdict(list)
+    vpc_refs = defaultdict(set)
 
     def __init__(self):
         self.vpcs = {}
-        self.__refs__[self.__class__].append(weakref.ref(self))
+        self.vpc_refs[self.__class__].add(weakref.ref(self))
         super(VPCBackend, self).__init__()
 
     @classmethod
-    def get_instances(cls):
-        for inst_ref in cls.__refs__[cls]:
+    def get_vpc_refs(cls):
+        for inst_ref in cls.vpc_refs[cls]:
             inst = inst_ref()
             if inst is not None:
                 yield inst
@@ -2159,7 +2161,7 @@ def get_vpc(self, vpc_id):
 
     # get vpc by vpc id and aws region
     def get_cross_vpc(self, vpc_id, peer_region):
-        for vpcs in self.get_instances():
+        for vpcs in self.get_vpc_refs():
             if vpcs.region_name == peer_region:
                 match_vpc = vpcs.get_vpc(vpc_id)
         return match_vpc
@@ -2280,15 +2282,31 @@ def physical_resource_id(self):
 
 
 class VPCPeeringConnectionBackend(object):
+    # for cross region vpc reference
+    vpc_pcx_refs = defaultdict(set)
+
     def __init__(self):
         self.vpc_pcxs = {}
+        self.vpc_pcx_refs[self.__class__].add(weakref.ref(self))
         super(VPCPeeringConnectionBackend, self).__init__()
 
+    @classmethod
+    def get_vpc_pcx_refs(cls):
+        for inst_ref in cls.vpc_pcx_refs[cls]:
+            inst = inst_ref()
+            if inst is not None:
+                yield inst
+
     def create_vpc_peering_connection(self, vpc, peer_vpc):
         vpc_pcx_id = random_vpc_peering_connection_id()
         vpc_pcx = VPCPeeringConnection(vpc_pcx_id, vpc, peer_vpc)
         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
         return vpc_pcx
 
     def get_all_vpc_peering_connections(self):
@@ -2306,6 +2324,11 @@ def delete_vpc_peering_connection(self, vpc_pcx_id):
 
     def accept_vpc_peering_connection(self, vpc_pcx_id):
         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
+        if pcx_req_region != pcx_acp_region and self.region_name == pcx_req_region:
+            raise OperationNotPermitted2(self.region_name, vpc_pcx.id, pcx_acp_region)
         if vpc_pcx._status.code != 'pending-acceptance':
             raise InvalidVPCPeeringConnectionStateTransitionError(vpc_pcx.id)
         vpc_pcx._status.accept()
@@ -2313,6 +2336,11 @@ def accept_vpc_peering_connection(self, vpc_pcx_id):
 
     def reject_vpc_peering_connection(self, vpc_pcx_id):
         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
+        if pcx_req_region != pcx_acp_region and self.region_name == pcx_req_region:
+            raise OperationNotPermitted3(self.region_name, vpc_pcx.id, pcx_acp_region)
         if vpc_pcx._status.code != 'pending-acceptance':
             raise InvalidVPCPeeringConnectionStateTransitionError(vpc_pcx.id)
         vpc_pcx._status.reject()
diff --git a/moto/ec2/responses/vpc_peering_connections.py b/moto/ec2/responses/vpc_peering_connections.py
--- a/moto/ec2/responses/vpc_peering_connections.py
+++ b/moto/ec2/responses/vpc_peering_connections.py
@@ -74,30 +74,35 @@ def reject_vpc_peering_connection(self):
 """
 
 DESCRIBE_VPC_PEERING_CONNECTIONS_RESPONSE = """
-<DescribeVpcPeeringConnectionsResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
-  <requestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</requestId>
-  <vpcPeeringConnectionSet>
-    {% for vpc_pcx in vpc_pcxs %}
-      <item>
-        <vpcPeeringConnectionId>{{ vpc_pcx.id }}</vpcPeeringConnectionId>
-        <requesterVpcInfo>
-          <ownerId>777788889999</ownerId>
-          <vpcId>{{ vpc_pcx.vpc.id }}</vpcId>
-          <cidrBlock>{{ vpc_pcx.vpc.cidr_block }}</cidrBlock>
-        </requesterVpcInfo>
-        <accepterVpcInfo>
-          <ownerId>123456789012</ownerId>
-          <vpcId>{{ vpc_pcx.peer_vpc.id }}</vpcId>
-        </accepterVpcInfo>
-        <status>
-          <code>{{ vpc_pcx._status.code }}</code>
-          <message>{{ vpc_pcx._status.message }}</message>
-        </status>
-        <expirationTime>2014-02-17T16:00:50.000Z</expirationTime>
-        <tagSet/>
-      </item>
-    {% endfor %}
-  </vpcPeeringConnectionSet>
+<DescribeVpcPeeringConnectionsResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
+<requestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</requestId>
+ <vpcPeeringConnectionSet>
+ {% for vpc_pcx in vpc_pcxs %}
+ <item>
+  <vpcPeeringConnectionId>{{ vpc_pcx.id }}</vpcPeeringConnectionId>
+    <requesterVpcInfo>
+     <ownerId>777788889999</ownerId>
+     <vpcId>{{ vpc_pcx.vpc.id }}</vpcId>
+     <cidrBlock>{{ vpc_pcx.vpc.cidr_block }}</cidrBlock>
+    </requesterVpcInfo>
+    <accepterVpcInfo>
+     <ownerId>123456789012</ownerId>
+     <vpcId>{{ vpc_pcx.peer_vpc.id }}</vpcId>
+     <cidrBlock>{{ vpc_pcx.peer_vpc.cidr_block }}</cidrBlock>
+     <peeringOptions>
+        <allowEgressFromLocalClassicLinkToRemoteVpc>false</allowEgressFromLocalClassicLinkToRemoteVpc>
+        <allowEgressFromLocalVpcToRemoteClassicLink>true</allowEgressFromLocalVpcToRemoteClassicLink>
+        <allowDnsResolutionFromRemoteVpc>false</allowDnsResolutionFromRemoteVpc>
+     </peeringOptions>
+    </accepterVpcInfo>
+     <status>
+      <code>{{ vpc_pcx._status.code }}</code>
+      <message>{{ vpc_pcx._status.message }}</message>
+     </status>
+     <tagSet/>
+ </item>
+ {% endfor %}
+ </vpcPeeringConnectionSet>
 </DescribeVpcPeeringConnectionsResponse>
 """
 
@@ -109,19 +114,24 @@ def reject_vpc_peering_connection(self):
 """
 
 ACCEPT_VPC_PEERING_CONNECTION_RESPONSE = """
-<AcceptVpcPeeringConnectionResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
+<AcceptVpcPeeringConnectionResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
   <requestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</requestId>
   <vpcPeeringConnection>
     <vpcPeeringConnectionId>{{ vpc_pcx.id }}</vpcPeeringConnectionId>
     <requesterVpcInfo>
-      <ownerId>123456789012</ownerId>
+      <ownerId>777788889999</ownerId>
       <vpcId>{{ vpc_pcx.vpc.id }}</vpcId>
       <cidrBlock>{{ vpc_pcx.vpc.cidr_block }}</cidrBlock>
     </requesterVpcInfo>
     <accepterVpcInfo>
-      <ownerId>777788889999</ownerId>
+      <ownerId>123456789012</ownerId>
       <vpcId>{{ vpc_pcx.peer_vpc.id }}</vpcId>
       <cidrBlock>{{ vpc_pcx.peer_vpc.cidr_block }}</cidrBlock>
+      <peeringOptions>
+        <allowEgressFromLocalClassicLinkToRemoteVpc>false</allowEgressFromLocalClassicLinkToRemoteVpc>
+        <allowEgressFromLocalVpcToRemoteClassicLink>false</allowEgressFromLocalVpcToRemoteClassicLink>
+        <allowDnsResolutionFromRemoteVpc>false</allowDnsResolutionFromRemoteVpc>
+      </peeringOptions>
     </accepterVpcInfo>
     <status>
       <code>{{ vpc_pcx._status.code }}</code>


Test patch:
diff --git a/tests/test_ec2/test_vpc_peering.py b/tests/test_ec2/test_vpc_peering.py
--- a/tests/test_ec2/test_vpc_peering.py
+++ b/tests/test_ec2/test_vpc_peering.py
@@ -107,14 +107,19 @@ def test_vpc_peering_connections_cross_region():
     ec2_apn1 = boto3.resource('ec2', region_name='ap-northeast-1')
     vpc_apn1 = ec2_apn1.create_vpc(CidrBlock='10.20.0.0/16')
     # create peering
-    vpc_pcx = ec2_usw1.create_vpc_peering_connection(
+    vpc_pcx_usw1 = ec2_usw1.create_vpc_peering_connection(
         VpcId=vpc_usw1.id,
         PeerVpcId=vpc_apn1.id,
         PeerRegion='ap-northeast-1',
     )
-    vpc_pcx.status['Code'].should.equal('initiating-request')
-    vpc_pcx.requester_vpc.id.should.equal(vpc_usw1.id)
-    vpc_pcx.accepter_vpc.id.should.equal(vpc_apn1.id)
+    vpc_pcx_usw1.status['Code'].should.equal('initiating-request')
+    vpc_pcx_usw1.requester_vpc.id.should.equal(vpc_usw1.id)
+    vpc_pcx_usw1.accepter_vpc.id.should.equal(vpc_apn1.id)
+    # test cross region vpc peering connection exist
+    vpc_pcx_apn1 = ec2_apn1.VpcPeeringConnection(vpc_pcx_usw1.id)
+    vpc_pcx_apn1.id.should.equal(vpc_pcx_usw1.id)
+    vpc_pcx_apn1.requester_vpc.id.should.equal(vpc_usw1.id)
+    vpc_pcx_apn1.accepter_vpc.id.should.equal(vpc_apn1.id)
 
 
 @mock_ec2
@@ -131,3 +136,148 @@ def test_vpc_peering_connections_cross_region_fail():
             PeerVpcId=vpc_apn1.id,
             PeerRegion='ap-northeast-2')
     cm.exception.response['Error']['Code'].should.equal('InvalidVpcID.NotFound')
+
+
+@mock_ec2
+def test_vpc_peering_connections_cross_region_accept():
+    # create vpc in us-west-1 and ap-northeast-1
+    ec2_usw1 = boto3.resource('ec2', region_name='us-west-1')
+    vpc_usw1 = ec2_usw1.create_vpc(CidrBlock='10.90.0.0/16')
+    ec2_apn1 = boto3.resource('ec2', region_name='ap-northeast-1')
+    vpc_apn1 = ec2_apn1.create_vpc(CidrBlock='10.20.0.0/16')
+    # create peering
+    vpc_pcx_usw1 = ec2_usw1.create_vpc_peering_connection(
+        VpcId=vpc_usw1.id,
+        PeerVpcId=vpc_apn1.id,
+        PeerRegion='ap-northeast-1',
+    )
+    # accept peering from ap-northeast-1
+    ec2_apn1 = boto3.client('ec2', region_name='ap-northeast-1')
+    ec2_usw1 = boto3.client('ec2', region_name='us-west-1')
+    acp_pcx_apn1 = ec2_apn1.accept_vpc_peering_connection(
+        VpcPeeringConnectionId=vpc_pcx_usw1.id
+    )
+    des_pcx_apn1 = ec2_usw1.describe_vpc_peering_connections(
+        VpcPeeringConnectionIds=[vpc_pcx_usw1.id]
+    )
+    des_pcx_usw1 = ec2_usw1.describe_vpc_peering_connections(
+        VpcPeeringConnectionIds=[vpc_pcx_usw1.id]
+    )
+    acp_pcx_apn1['VpcPeeringConnection']['Status']['Code'].should.equal('active')
+    des_pcx_apn1['VpcPeeringConnections'][0]['Status']['Code'].should.equal('active')
+    des_pcx_usw1['VpcPeeringConnections'][0]['Status']['Code'].should.equal('active')
+
+
+@mock_ec2
+def test_vpc_peering_connections_cross_region_reject():
+    # create vpc in us-west-1 and ap-northeast-1
+    ec2_usw1 = boto3.resource('ec2', region_name='us-west-1')
+    vpc_usw1 = ec2_usw1.create_vpc(CidrBlock='10.90.0.0/16')
+    ec2_apn1 = boto3.resource('ec2', region_name='ap-northeast-1')
+    vpc_apn1 = ec2_apn1.create_vpc(CidrBlock='10.20.0.0/16')
+    # create peering
+    vpc_pcx_usw1 = ec2_usw1.create_vpc_peering_connection(
+        VpcId=vpc_usw1.id,
+        PeerVpcId=vpc_apn1.id,
+        PeerRegion='ap-northeast-1',
+    )
+    # reject peering from ap-northeast-1
+    ec2_apn1 = boto3.client('ec2', region_name='ap-northeast-1')
+    ec2_usw1 = boto3.client('ec2', region_name='us-west-1')
+    rej_pcx_apn1 = ec2_apn1.reject_vpc_peering_connection(
+        VpcPeeringConnectionId=vpc_pcx_usw1.id
+    )
+    des_pcx_apn1 = ec2_usw1.describe_vpc_peering_connections(
+        VpcPeeringConnectionIds=[vpc_pcx_usw1.id]
+    )
+    des_pcx_usw1 = ec2_usw1.describe_vpc_peering_connections(
+        VpcPeeringConnectionIds=[vpc_pcx_usw1.id]
+    )
+    rej_pcx_apn1['Return'].should.equal(True)
+    des_pcx_apn1['VpcPeeringConnections'][0]['Status']['Code'].should.equal('rejected')
+    des_pcx_usw1['VpcPeeringConnections'][0]['Status']['Code'].should.equal('rejected')
+
+
+@mock_ec2
+def test_vpc_peering_connections_cross_region_delete():
+    # create vpc in us-west-1 and ap-northeast-1
+    ec2_usw1 = boto3.resource('ec2', region_name='us-west-1')
+    vpc_usw1 = ec2_usw1.create_vpc(CidrBlock='10.90.0.0/16')
+    ec2_apn1 = boto3.resource('ec2', region_name='ap-northeast-1')
+    vpc_apn1 = ec2_apn1.create_vpc(CidrBlock='10.20.0.0/16')
+    # create peering
+    vpc_pcx_usw1 = ec2_usw1.create_vpc_peering_connection(
+        VpcId=vpc_usw1.id,
+        PeerVpcId=vpc_apn1.id,
+        PeerRegion='ap-northeast-1',
+    )
+    # reject peering from ap-northeast-1
+    ec2_apn1 = boto3.client('ec2', region_name='ap-northeast-1')
+    ec2_usw1 = boto3.client('ec2', region_name='us-west-1')
+    del_pcx_apn1 = ec2_apn1.delete_vpc_peering_connection(
+        VpcPeeringConnectionId=vpc_pcx_usw1.id
+    )
+    des_pcx_apn1 = ec2_usw1.describe_vpc_peering_connections(
+        VpcPeeringConnectionIds=[vpc_pcx_usw1.id]
+    )
+    des_pcx_usw1 = ec2_usw1.describe_vpc_peering_connections(
+        VpcPeeringConnectionIds=[vpc_pcx_usw1.id]
+    )
+    del_pcx_apn1['Return'].should.equal(True)
+    des_pcx_apn1['VpcPeeringConnections'][0]['Status']['Code'].should.equal('deleted')
+    des_pcx_usw1['VpcPeeringConnections'][0]['Status']['Code'].should.equal('deleted')
+
+
+@mock_ec2
+def test_vpc_peering_connections_cross_region_accept_wrong_region():
+    # create vpc in us-west-1 and ap-northeast-1
+    ec2_usw1 = boto3.resource('ec2', region_name='us-west-1')
+    vpc_usw1 = ec2_usw1.create_vpc(CidrBlock='10.90.0.0/16')
+    ec2_apn1 = boto3.resource('ec2', region_name='ap-northeast-1')
+    vpc_apn1 = ec2_apn1.create_vpc(CidrBlock='10.20.0.0/16')
+    # create peering
+    vpc_pcx_usw1 = ec2_usw1.create_vpc_peering_connection(
+        VpcId=vpc_usw1.id,
+        PeerVpcId=vpc_apn1.id,
+        PeerRegion='ap-northeast-1',
+    )
+
+    # accept wrong peering from us-west-1 which will raise error
+    ec2_apn1 = boto3.client('ec2', region_name='ap-northeast-1')
+    ec2_usw1 = boto3.client('ec2', region_name='us-west-1')
+    with assert_raises(ClientError) as cm:
+        ec2_usw1.accept_vpc_peering_connection(
+            VpcPeeringConnectionId=vpc_pcx_usw1.id
+        )
+    cm.exception.response['Error']['Code'].should.equal('OperationNotPermitted')
+    exp_msg = 'Incorrect region ({0}) specified for this request.VPC ' \
+              'peering connection {1} must be ' \
+              'accepted in region {2}'.format('us-west-1', vpc_pcx_usw1.id, 'ap-northeast-1')
+    cm.exception.response['Error']['Message'].should.equal(exp_msg)
+
+
+@mock_ec2
+def test_vpc_peering_connections_cross_region_reject_wrong_region():
+    # create vpc in us-west-1 and ap-northeast-1
+    ec2_usw1 = boto3.resource('ec2', region_name='us-west-1')
+    vpc_usw1 = ec2_usw1.create_vpc(CidrBlock='10.90.0.0/16')
+    ec2_apn1 = boto3.resource('ec2', region_name='ap-northeast-1')
+    vpc_apn1 = ec2_apn1.create_vpc(CidrBlock='10.20.0.0/16')
+    # create peering
+    vpc_pcx_usw1 = ec2_usw1.create_vpc_peering_connection(
+        VpcId=vpc_usw1.id,
+        PeerVpcId=vpc_apn1.id,
+        PeerRegion='ap-northeast-1',
+    )
+    # reject wrong peering from us-west-1 which will raise error
+    ec2_apn1 = boto3.client('ec2', region_name='ap-northeast-1')
+    ec2_usw1 = boto3.client('ec2', region_name='us-west-1')
+    with assert_raises(ClientError) as cm:
+        ec2_usw1.reject_vpc_peering_connection(
+            VpcPeeringConnectionId=vpc_pcx_usw1.id
+        )
+    cm.exception.response['Error']['Code'].should.equal('OperationNotPermitted')
+    exp_msg = 'Incorrect region ({0}) specified for this request.VPC ' \
+              'peering connection {1} must be accepted or ' \
+              'rejected in region {2}'.format('us-west-1', vpc_pcx_usw1.id, 'ap-northeast-1')
+    cm.exception.response['Error']['Message'].should.equal(exp_msg)