diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py
index 682253897..fed04ecbb 100644
--- a/moto/ec2/exceptions.py
+++ b/moto/ec2/exceptions.py
@@ -37,6 +37,22 @@ class InvalidVPCIdError(EC2ClientError):
.format(vpc_id))
+class InvalidVPCPeeringConnectionIdError(EC2ClientError):
+ def __init__(self, vpc_peering_connection_id):
+ super(InvalidVPCPeeringConnectionIdError, self).__init__(
+ "InvalidVpcPeeringConnectionId.NotFound",
+ "VpcPeeringConnectionID {0} does not exist."
+ .format(vpc_peering_connection_id))
+
+
+class InvalidVPCPeeringConnectionStateTransitionError(EC2ClientError):
+ def __init__(self, vpc_peering_connection_id):
+ super(InvalidVPCPeeringConnectionStateTransitionError, self).__init__(
+ "InvalidStateTransition",
+ "VpcPeeringConnectionID {0} is not in the correct state for the request."
+ .format(vpc_peering_connection_id))
+
+
class InvalidParameterValueError(EC2ClientError):
def __init__(self, parameter_value):
super(InvalidParameterValueError, self).__init__(
diff --git a/moto/ec2/models.py b/moto/ec2/models.py
index c25f49e28..b91bc8d57 100644
--- a/moto/ec2/models.py
+++ b/moto/ec2/models.py
@@ -12,7 +12,9 @@ from .exceptions import (
InvalidInternetGatewayIDError,
GatewayNotAttachedError,
ResourceAlreadyAssociatedError,
- InvalidVPCIdError
+ InvalidVPCIdError,
+ InvalidVPCPeeringConnectionIdError,
+ InvalidVPCPeeringConnectionStateTransitionError
)
from .utils import (
random_ami_id,
@@ -32,6 +34,7 @@ from .utils import (
random_subnet_id,
random_volume_id,
random_vpc_id,
+ random_vpc_peering_connection_id,
)
@@ -712,6 +715,89 @@ class VPCBackend(object):
return vpc
+class VPCPeeringConnectionStatus(object):
+ def __init__(self, code='initiating-request', message=''):
+ self.code = code
+ self.message = message
+
+ def initiating(self):
+ self.code = 'initiating-request'
+ self.message = 'Initiating Request to {accepter ID}'
+
+ def pending(self):
+ self.code = 'pending-acceptance'
+ self.message = 'Pending Acceptance by {accepter ID}'
+
+ def accept(self):
+ self.code = 'active'
+ self.message = 'Active'
+
+ def reject(self):
+ self.code = 'rejected'
+ self.message = 'Inactive'
+
+
+class VPCPeeringConnection(TaggedEC2Instance):
+ def __init__(self, vpc_pcx_id, vpc, peer_vpc):
+ self.id = vpc_pcx_id
+ self.vpc = vpc
+ self.peer_vpc = peer_vpc
+ self._status = VPCPeeringConnectionStatus()
+
+ @classmethod
+ def create_from_cloudformation_json(cls, resource_name, cloudformation_json):
+ properties = cloudformation_json['Properties']
+
+ vpc = self.get_vpc(properties['VpcId'])
+ peer_vpc = self.get_vpc(properties['PeerVpcId'])
+
+ vpc_pcx = ec2_backend.create_vpc_peering_connection(vpc, peer_vpc)
+
+ return vpc_pcx
+
+ @property
+ def physical_resource_id(self):
+ return self.id
+
+
+class VPCPeeringConnectionBackend(object):
+ def __init__(self):
+ self.vpc_pcxs = {}
+ super(VPCPeeringConnectionBackend, self).__init__()
+
+ 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
+ return vpc_pcx
+
+ def get_all_vpc_peering_connections(self):
+ return self.vpc_pcxs.values()
+
+ def get_vpc_peering_connection(self, vpc_pcx_id):
+ if vpc_pcx_id not in self.vpc_pcxs:
+ raise InvalidVPCPeeringConnectionIdError(vpc_pcx_id)
+ return self.vpc_pcxs.get(vpc_pcx_id)
+
+ def delete_vpc_peering_connection(self, vpc_pcx_id):
+ return self.vpc_pcxs.pop(vpc_pcx_id, None)
+
+ def accept_vpc_peering_connection(self, vpc_pcx_id):
+ vpc_pcx = self.get_vpc_peering_connection(vpc_pcx_id)
+ 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):
+ vpc_pcx = self.get_vpc_peering_connection(vpc_pcx_id)
+ if vpc_pcx._status.code != 'pending-acceptance':
+ raise InvalidVPCPeeringConnectionStateTransitionError(vpc_pcx.id)
+ vpc_pcx._status.reject()
+ return vpc_pcx
+
+
class Subnet(TaggedEC2Instance):
def __init__(self, subnet_id, vpc_id, cidr_block):
self.id = subnet_id
@@ -1168,6 +1254,7 @@ class DHCPOptionsSetBackend(object):
class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend,
RegionsAndZonesBackend, SecurityGroupBackend, EBSBackend,
VPCBackend, SubnetBackend, SubnetRouteTableAssociationBackend,
+ VPCPeeringConnectionBackend,
RouteTableBackend, RouteBackend, InternetGatewayBackend,
VPCGatewayAttachmentBackend, SpotRequestBackend,
ElasticAddressBackend, KeyPairBackend, DHCPOptionsSetBackend):
diff --git a/moto/ec2/responses/__init__.py b/moto/ec2/responses/__init__.py
index cebff3eba..fa487293f 100644
--- a/moto/ec2/responses/__init__.py
+++ b/moto/ec2/responses/__init__.py
@@ -24,6 +24,7 @@ from .virtual_private_gateways import VirtualPrivateGateways
from .vm_export import VMExport
from .vm_import import VMImport
from .vpcs import VPCs
+from .vpc_peering_connections import VPCPeeringConnections
from .vpn_connections import VPNConnections
from .windows import Windows
@@ -55,6 +56,7 @@ class EC2Response(
VMExport,
VMImport,
VPCs,
+ VPCPeeringConnections,
VPNConnections,
Windows,
):
diff --git a/moto/ec2/responses/vpc_peering_connections.py b/moto/ec2/responses/vpc_peering_connections.py
new file mode 100644
index 000000000..786332672
--- /dev/null
+++ b/moto/ec2/responses/vpc_peering_connections.py
@@ -0,0 +1,137 @@
+from jinja2 import Template
+
+from moto.core.responses import BaseResponse
+from moto.ec2.models import ec2_backend
+
+
+class VPCPeeringConnections(BaseResponse):
+ def create_vpc_peering_connection(self):
+ vpc = ec2_backend.get_vpc(self.querystring.get('VpcId')[0])
+ peer_vpc = ec2_backend.get_vpc(self.querystring.get('PeerVpcId')[0])
+ vpc_pcx = ec2_backend.create_vpc_peering_connection(vpc, peer_vpc)
+ template = Template(CREATE_VPC_PEERING_CONNECTION_RESPONSE)
+ return template.render(vpc_pcx=vpc_pcx)
+
+ def delete_vpc_peering_connection(self):
+ vpc_pcx_id = self.querystring.get('VpcPeeringConnectionId')[0]
+ vpc_pcx = ec2_backend.delete_vpc_peering_connection(vpc_pcx_id)
+ if vpc_pcx:
+ template = Template(DELETE_VPC_PEERING_CONNECTION_RESPONSE)
+ return template.render(vpc_pcx=vpc_pcx)
+ else:
+ return "", dict(status=404)
+
+ def describe_vpc_peering_connections(self):
+ vpc_pcxs = ec2_backend.get_all_vpc_peering_connections()
+ template = Template(DESCRIBE_VPC_PEERING_CONNECTIONS_RESPONSE)
+ return template.render(vpc_pcxs=vpc_pcxs)
+
+ def accept_vpc_peering_connection(self):
+ vpc_pcx_id = self.querystring.get('VpcPeeringConnectionId')[0]
+ vpc_pcx = ec2_backend.accept_vpc_peering_connection(vpc_pcx_id)
+ if vpc_pcx:
+ template = Template(ACCEPT_VPC_PEERING_CONNECTION_RESPONSE)
+ return template.render(vpc_pcx=vpc_pcx)
+ else:
+ return "", dict(status=404)
+
+ def reject_vpc_peering_connection(self):
+ vpc_pcx_id = self.querystring.get('VpcPeeringConnectionId')[0]
+ vpc_pcx = ec2_backend.reject_vpc_peering_connection(vpc_pcx_id)
+ if vpc_pcx:
+ template = Template(REJECT_VPC_PEERING_CONNECTION_RESPONSE)
+ return template.render()
+ else:
+ return "", dict(status=404)
+
+
+CREATE_VPC_PEERING_CONNECTION_RESPONSE = """
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+ {{ vpc_pcx.id }}
+
+ 777788889999
+ {{ vpc_pcx.vpc.id }}
+ {{ vpc_pcx.vpc.cidr_block }}
+
+
+ 123456789012
+ {{ vpc_pcx.peer_vpc.id }}
+
+
+ initiating-request
+ Initiating request to {accepter ID}.
+
+ 2014-02-18T14:37:25.000Z
+
+
+
+"""
+
+DESCRIBE_VPC_PEERING_CONNECTIONS_RESPONSE = """
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+ {% for vpc_pcx in vpc_pcxs %}
+ -
+ {{ vpc_pcx.id }}
+
+ 777788889999
+ {{ vpc_pcx.vpc.id }}
+ {{ vpc_pcx.vpc.cidr_block }}
+
+
+ 111122223333
+ {{ vpc_pcx.peer_vpc.id }}
+
+
+
{{ vpc_pcx._status.code }}
+ {{ vpc_pcx._status.message }}
+
+ 2014-02-17T16:00:50.000Z
+
+
+ {% endfor %}
+
+
+"""
+
+DELETE_VPC_PEERING_CONNECTION_RESPONSE = """
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+ true
+
+"""
+
+ACCEPT_VPC_PEERING_CONNECTION_RESPONSE = """
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+
+ {{ vpc_pcx.id }}
+
+ 123456789012
+ {{ vpc_pcx.vpc.id }}
+ {{ vpc_pcx.vpc.cidr_block }}
+
+
+ 777788889999
+ {{ vpc_pcx.peer_vpc.id }}
+ {{ vpc_pcx.peer_vpc.cidr_block }}
+
+
+ {{ vpc_pcx._status.code }}
+ {{ vpc_pcx._status.message }}
+
+
+
+
+"""
+
+REJECT_VPC_PEERING_CONNECTION_RESPONSE = """
+
+ 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
+ true
+
+"""
+
diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py
index 516f3bec8..77dce0d1e 100644
--- a/moto/ec2/utils.py
+++ b/moto/ec2/utils.py
@@ -46,6 +46,10 @@ def random_vpc_id():
return random_id(prefix='vpc')
+def random_vpc_peering_connection_id():
+ return random_id(prefix='pcx')
+
+
def random_eip_association_id():
return random_id(prefix='eipassoc')
diff --git a/tests/test_ec2/test_vpc_peering.py b/tests/test_ec2/test_vpc_peering.py
new file mode 100644
index 000000000..79c2fd3d5
--- /dev/null
+++ b/tests/test_ec2/test_vpc_peering.py
@@ -0,0 +1,76 @@
+import boto
+from boto.exception import EC2ResponseError
+import sure # noqa
+
+from moto import mock_ec2
+
+
+@mock_ec2
+def test_vpc_peering_connections():
+ conn = boto.connect_vpc('the_key', 'the_secret')
+ vpc = conn.create_vpc("10.0.0.0/16")
+ peer_vpc = conn.create_vpc("11.0.0.0/16")
+
+ vpc_pcx = conn.create_vpc_peering_connection(vpc.id, peer_vpc.id)
+ vpc_pcx._status.code.should.equal('initiating-request')
+
+ return vpc_pcx
+
+
+@mock_ec2
+def test_vpc_peering_connections_get_all():
+ conn = boto.connect_vpc('the_key', 'the_secret')
+ vpc_pcx = test_vpc_peering_connections()
+ vpc_pcx._status.code.should.equal('initiating-request')
+
+ all_vpc_pcxs = conn.get_all_vpc_peering_connections()
+ all_vpc_pcxs.should.have.length_of(1)
+ all_vpc_pcxs[0]._status.code.should.equal('pending-acceptance')
+
+
+@mock_ec2
+def test_vpc_peering_connections_accept():
+ conn = boto.connect_vpc('the_key', 'the_secret')
+ vpc_pcx = test_vpc_peering_connections()
+
+ vpc_pcx = conn.accept_vpc_peering_connection(vpc_pcx.id)
+ vpc_pcx._status.code.should.equal('active')
+
+ conn.reject_vpc_peering_connection.when.called_with(
+ vpc_pcx.id).should.throw(EC2ResponseError)
+
+ all_vpc_pcxs = conn.get_all_vpc_peering_connections()
+ all_vpc_pcxs.should.have.length_of(1)
+ all_vpc_pcxs[0]._status.code.should.equal('active')
+
+
+@mock_ec2
+def test_vpc_peering_connections_reject():
+ conn = boto.connect_vpc('the_key', 'the_secret')
+ vpc_pcx = test_vpc_peering_connections()
+
+ verdict = conn.reject_vpc_peering_connection(vpc_pcx.id)
+ verdict.should.equal(True)
+
+ conn.accept_vpc_peering_connection.when.called_with(
+ vpc_pcx.id).should.throw(EC2ResponseError)
+
+ all_vpc_pcxs = conn.get_all_vpc_peering_connections()
+ all_vpc_pcxs.should.have.length_of(1)
+ all_vpc_pcxs[0]._status.code.should.equal('rejected')
+
+
+@mock_ec2
+def test_vpc_peering_connections_delete():
+ conn = boto.connect_vpc('the_key', 'the_secret')
+ vpc_pcx = test_vpc_peering_connections()
+
+ verdict = vpc_pcx.delete()
+ verdict.should.equal(True)
+
+ all_vpc_pcxs = conn.get_all_vpc_peering_connections()
+ all_vpc_pcxs.should.have.length_of(0)
+
+ conn.delete_vpc_peering_connection.when.called_with(
+ "pcx-1234abcd").should.throw(EC2ResponseError)
+