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) +