From 54690d10956781254f981f939087b33a5f4a969f Mon Sep 17 00:00:00 2001 From: Feodor Tersin Date: Sat, 16 May 2015 12:34:57 +0300 Subject: [PATCH] Implement customer gateway Change-Id: I0e090f1d8dca70235615242f8993081c71ae6615 --- ec2api/api/cloud.py | 56 ++++++++++ ec2api/api/common.py | 12 ++- ec2api/api/customer_gateway.py | 76 +++++++++++++ ec2api/api/ec2utils.py | 1 + ec2api/api/validator.py | 8 ++ ec2api/exception.py | 5 + ec2api/tests/unit/fakes.py | 38 +++++++ ec2api/tests/unit/test_customer_gateway.py | 119 +++++++++++++++++++++ ec2api/tests/unit/test_ec2_validate.py | 12 +++ ec2api/tests/unit/test_ec2utils.py | 2 + 10 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 ec2api/api/customer_gateway.py create mode 100644 ec2api/tests/unit/test_customer_gateway.py diff --git a/ec2api/api/cloud.py b/ec2api/api/cloud.py index 01685ce8..6bc8078a 100644 --- a/ec2api/api/cloud.py +++ b/ec2api/api/cloud.py @@ -27,6 +27,7 @@ from oslo_log import log as logging from ec2api.api import address from ec2api.api import availability_zone +from ec2api.api import customer_gateway from ec2api.api import dhcp_options from ec2api.api import image from ec2api.api import instance @@ -1775,3 +1776,58 @@ class VpcCloudController(CloudController): Returns: true if the request succeeds. """ + + @module_and_param_types(customer_gateway, 'ip', 'vpn_connection_type', + 'int') + def create_customer_gateway(self, context, ip_address, type, + bgp_asn=None): + """Provides information to EC2 API about VPN customer gateway device. + + Args: + context (RequestContext): The request context. + ip_address (str): The Internet-routable IP address for the + customer gateway's outside interface. + type (str): The type of VPN connection that this customer gateway + supports (ipsec.1). + bgp_asn (int): For devices that support BGP, + the customer gateway's BGP ASN (65000 otherwise). + + Returns: + Information about the customer gateway. + + You cannot create more than one customer gateway with the same VPN + type, IP address, and BGP ASN parameter values. If you run an + identical request more than one time, subsequent requests return + information about the existing customer gateway. + """ + + @module_and_param_types(customer_gateway, 'cgw_id') + def delete_customer_gateway(self, context, customer_gateway_id): + """Deletes the specified customer gateway. + + Args: + context (RequestContext): The request context. + customer_gateway_id (str): The ID of the customer gateway. + + Returns: + true if the request succeeds. + + You must delete the VPN connection before you can delete the customer + gateway. + """ + + @module_and_param_types(customer_gateway, 'cgw_ids', + 'filter') + def describe_customer_gateways(self, context, customer_gateway_id=None, + filter=None): + """Describes one or more of your VPN customer gateways. + + Args: + context (RequestContext): The request context. + customer_gateway_id (list of str): One or more customer gateway + IDs. + filter (list of filter dict): One or more filters. + + Returns: + Information about one or more customer gateways. + """ diff --git a/ec2api/api/common.py b/ec2api/api/common.py index ce05d0fd..ea1814d2 100644 --- a/ec2api/api/common.py +++ b/ec2api/api/common.py @@ -247,6 +247,12 @@ class Validator(object): def dopt_ids(self, ids): self.multi(ids, self.dopt_id) + def cgw_id(self, id): + self.ec2_id(id, ['cgw']) + + def cgw_ids(self, ids): + self.multi(ids, self.cgw_id) + def security_group_str(self, value): validator.validate_security_group_str(value, self.param_name, self.params.get('vpc_id')) @@ -254,8 +260,12 @@ class Validator(object): def security_group_strs(self, values): self.multi(values, self.security_group_str) + def vpn_connection_type(self, value): + validator.validate_vpn_connection_type(value) -VPC_KINDS = ['vpc', 'igw', 'subnet', 'eni', 'dopt', 'eipalloc', 'sg', 'rtb'] + +VPC_KINDS = ['vpc', 'igw', 'subnet', 'eni', 'dopt', 'eipalloc', 'sg', 'rtb', + 'cgw'] class UniversalDescriber(object): diff --git a/ec2api/api/customer_gateway.py b/ec2api/api/customer_gateway.py new file mode 100644 index 00000000..368aecf8 --- /dev/null +++ b/ec2api/api/customer_gateway.py @@ -0,0 +1,76 @@ +# Copyright 2014 +# The Cloudscaling Group, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ec2api.api import common +from ec2api.api import ec2utils +from ec2api.db import api as db_api +from ec2api import exception +from ec2api.i18n import _ + + +Validator = common.Validator + + +DEFAULT_BGP_ASN = 65000 + + +def create_customer_gateway(context, ip_address, type, bgp_asn=None): + if bgp_asn and bgp_asn != DEFAULT_BGP_ASN: + raise exception.Unsupported("BGP dynamic routing is unsupported") + customer_gateway = next((cgw for cgw in db_api.get_items(context, 'cgw') + if cgw['ip_address'] == ip_address), None) + if not customer_gateway: + customer_gateway = db_api.add_item(context, 'cgw', + {'ip_address': ip_address}) + return {'customerGateway': _format_customer_gateway(customer_gateway)} + + +def delete_customer_gateway(context, customer_gateway_id): + customer_gateway = ec2utils.get_db_item(context, customer_gateway_id) + vpn_connections = db_api.get_items(context, 'vpn') + if any(vpn['customer_gateway_id'] == customer_gateway['id'] + for vpn in vpn_connections): + raise exception.IncorrectState( + reason=_('The customer gateway is in use.')) + db_api.delete_item(context, customer_gateway['id']) + return True + + +def describe_customer_gateways(context, customer_gateway_id=None, + filter=None): + formatted_cgws = CustomerGatewayDescriber().describe( + context, ids=customer_gateway_id, filter=filter) + return {'customerGatewaySet': formatted_cgws} + + +class CustomerGatewayDescriber(common.TaggableItemsDescriber, + common.NonOpenstackItemsDescriber): + + KIND = 'cgw' + FILTER_MAP = {'bgp-asn': 'bgpAsn', + 'customer-gateway-id': 'customerGatewayId', + 'ip-address': 'ipAddress', + 'state': 'state', + 'type': 'type'} + + def format(self, customer_gateway): + return _format_customer_gateway(customer_gateway) + + +def _format_customer_gateway(customer_gateway): + return {'customerGatewayId': customer_gateway['id'], + 'ipAddress': customer_gateway['ip_address'], + 'state': 'available', + 'type': 'ipsec.1', + 'bgpAsn': DEFAULT_BGP_ASN} diff --git a/ec2api/api/ec2utils.py b/ec2api/api/ec2utils.py index f5d4186b..f375c188 100644 --- a/ec2api/api/ec2utils.py +++ b/ec2api/api/ec2utils.py @@ -189,6 +189,7 @@ NOT_FOUND_EXCEPTION_MAP = { 'ami': exception.InvalidAMIIDNotFound, 'aki': exception.InvalidAMIIDNotFound, 'ari': exception.InvalidAMIIDNotFound, + 'cgw': exception.InvalidCustomerGatewayIDNotFound, } diff --git a/ec2api/api/validator.py b/ec2api/api/validator.py index 54a592f3..2ee1ab2f 100644 --- a/ec2api/api/validator.py +++ b/ec2api/api/validator.py @@ -224,3 +224,11 @@ def validate_security_group_str(value, parameter_name, vpc_id=None): if msg: raise exception.ValidationError(reason=msg) return True + + +def validate_vpn_connection_type(value): + if value != 'ipsec.1': + raise exception.InvalidParameterValue( + value=type, parameter='type', + reason=_('Invalid VPN connection type.')) + return True diff --git a/ec2api/exception.py b/ec2api/exception.py index c486f8ed..efc2ec7b 100644 --- a/ec2api/exception.py +++ b/ec2api/exception.py @@ -400,6 +400,11 @@ class InvalidAvailabilityZoneNotFound(EC2NotFoundException): msg_fmt = _("Availability zone %(id)s not found") +class InvalidCustomerGatewayIDNotFound(EC2NotFoundException): + ec2_code = 'InvalidCustomerGatewayID.NotFound' + msg_fmt = _("The customerGateway ID '%(id)s' does not exist") + + class ResourceLimitExceeded(EC2OverlimitException): msg_fmt = _('You have reached the limit of %(resource)s') diff --git a/ec2api/tests/unit/fakes.py b/ec2api/tests/unit/fakes.py index a69e9ac3..df384f52 100644 --- a/ec2api/tests/unit/fakes.py +++ b/ec2api/tests/unit/fakes.py @@ -234,6 +234,14 @@ FINGERPRINT_KEY_PAIR = ( '2a:72:dd:aa:0d:a6:45:4d:27:4f:75:28:73:0d:a6:10:35:88:e1:ce') +# customer gateway constants +ID_EC2_CUSTOMER_GATEWAY_1 = random_ec2_id('cgw') +ID_EC2_CUSTOMER_GATEWAY_2 = random_ec2_id('cgw') + +IP_CUSTOMER_GATEWAY_ADDRESS_1 = '172.16.1.11' +IP_CUSTOMER_GATEWAY_ADDRESS_2 = '172.31.2.22' + + # Object constants section # Constant name notation: # [] @@ -1489,6 +1497,36 @@ EC2_KEY_PAIR = {'keyName': NAME_KEY_PAIR, 'keyMaterial': PRIVATE_KEY_KEY_PAIR} +# customer gateway objects +DB_CUSTOMER_GATEWAY_1 = { + 'id': ID_EC2_CUSTOMER_GATEWAY_1, + 'ip_address': IP_CUSTOMER_GATEWAY_ADDRESS_1, + 'os_id': None, + 'vpc_id': None, +} +DB_CUSTOMER_GATEWAY_2 = { + 'id': ID_EC2_CUSTOMER_GATEWAY_2, + 'ip_address': IP_CUSTOMER_GATEWAY_ADDRESS_2, + 'os_id': None, + 'vpc_id': None, +} + +EC2_CUSTOMER_GATEWAY_1 = { + 'customerGatewayId': ID_EC2_CUSTOMER_GATEWAY_1, + 'ipAddress': IP_CUSTOMER_GATEWAY_ADDRESS_1, + 'state': 'available', + 'type': 'ipsec.1', + 'bgpAsn': 65000, +} +EC2_CUSTOMER_GATEWAY_2 = { + 'customerGatewayId': ID_EC2_CUSTOMER_GATEWAY_2, + 'ipAddress': IP_CUSTOMER_GATEWAY_ADDRESS_2, + 'state': 'available', + 'type': 'ipsec.1', + 'bgpAsn': 65000, +} + + # Object generator functions section # internet gateway generator functions diff --git a/ec2api/tests/unit/test_customer_gateway.py b/ec2api/tests/unit/test_customer_gateway.py new file mode 100644 index 00000000..20e13716 --- /dev/null +++ b/ec2api/tests/unit/test_customer_gateway.py @@ -0,0 +1,119 @@ +# Copyright 2014 +# The Cloudscaling Group, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import mock + +from ec2api.tests.unit import base +from ec2api.tests.unit import fakes +from ec2api.tests.unit import matchers +from ec2api.tests.unit import tools + + +class CustomerGatewayTestCase(base.ApiTestCase): + + def test_create_customer_gateway(self): + self.db_api.add_item.side_effect = ( + tools.get_db_api_add_item(fakes.ID_EC2_CUSTOMER_GATEWAY_2)) + + resp = self.execute('CreateCustomerGateway', + {'IpAddress': fakes.IP_CUSTOMER_GATEWAY_ADDRESS_2, + 'Type': 'ipsec.1'}) + self.assertEqual({'customerGateway': fakes.EC2_CUSTOMER_GATEWAY_2}, + resp) + self.db_api.add_item.assert_called_once_with( + mock.ANY, 'cgw', + {'ip_address': fakes.IP_CUSTOMER_GATEWAY_ADDRESS_2}, + project_id=None) + + resp = self.execute('CreateCustomerGateway', + {'IpAddress': fakes.IP_CUSTOMER_GATEWAY_ADDRESS_2, + 'Type': 'ipsec.1', + 'BgpAsn': '65000'}) + self.assertEqual({'customerGateway': fakes.EC2_CUSTOMER_GATEWAY_2}, + resp) + + def test_create_customer_gateway_idempotent(self): + self.set_mock_db_items(fakes.DB_CUSTOMER_GATEWAY_1) + + resp = self.execute('CreateCustomerGateway', + {'IpAddress': fakes.IP_CUSTOMER_GATEWAY_ADDRESS_1, + 'Type': 'ipsec.1'}) + self.assertEqual({'customerGateway': fakes.EC2_CUSTOMER_GATEWAY_1}, + resp) + self.assertFalse(self.db_api.add_item.called) + + resp = self.execute('CreateCustomerGateway', + {'IpAddress': fakes.IP_CUSTOMER_GATEWAY_ADDRESS_1, + 'Type': 'ipsec.1', + 'BgpAsn': '65000'}) + self.assertEqual({'customerGateway': fakes.EC2_CUSTOMER_GATEWAY_1}, + resp) + self.assertFalse(self.db_api.add_item.called) + + def test_create_customer_gateway_invalid_parameters(self): + self.assert_execution_error( + 'Unsupported', + 'CreateCustomerGateway', + {'IpAddress': fakes.IP_CUSTOMER_GATEWAY_ADDRESS_1, + 'Type': 'ipsec.1', + 'BgpAsn': '456'}) + + def test_delete_customer_gateway(self): + self.set_mock_db_items(fakes.DB_CUSTOMER_GATEWAY_2) + + resp = self.execute( + 'DeleteCustomerGateway', + {'CustomerGatewayId': fakes.ID_EC2_CUSTOMER_GATEWAY_2}) + + self.assertEqual({'return': True}, resp) + self.db_api.delete_item.assert_called_once_with( + mock.ANY, fakes.ID_EC2_CUSTOMER_GATEWAY_2) + + def test_delete_customer_gateway_invalid_parameters(self): + self.set_mock_db_items() + self.assert_execution_error( + 'InvalidCustomerGatewayID.NotFound', + 'DeleteCustomerGateway', + {'CustomerGatewayId': fakes.ID_EC2_CUSTOMER_GATEWAY_2}) + self.assertFalse(self.db_api.delete_item.called) + + def test_describe_customer_gateways(self): + self.set_mock_db_items(fakes.DB_CUSTOMER_GATEWAY_1, + fakes.DB_CUSTOMER_GATEWAY_2) + + resp = self.execute('DescribeCustomerGateways', {}) + self.assertThat(resp['customerGatewaySet'], + matchers.ListMatches([fakes.EC2_CUSTOMER_GATEWAY_1, + fakes.EC2_CUSTOMER_GATEWAY_2])) + + resp = self.execute( + 'DescribeCustomerGateways', + {'CustomerGatewayId.1': fakes.ID_EC2_CUSTOMER_GATEWAY_2}) + self.assertThat( + resp['customerGatewaySet'], + matchers.ListMatches([fakes.EC2_CUSTOMER_GATEWAY_2])) + self.db_api.get_items_by_ids.assert_called_once_with( + mock.ANY, set([fakes.ID_EC2_CUSTOMER_GATEWAY_2])) + + self.check_filtering( + 'DescribeCustomerGateways', 'customerGatewaySet', + [('bgp-asn', 65000), + ('customer-gateway-id', fakes.ID_EC2_CUSTOMER_GATEWAY_2), + ('ip-address', fakes.IP_CUSTOMER_GATEWAY_ADDRESS_2), + ('state', 'available'), + ('type', 'ipsec.1')]) + self.check_tag_support( + 'DescribeCustomerGateways', 'customerGatewaySet', + fakes.ID_EC2_CUSTOMER_GATEWAY_2, 'customerGatewayId') diff --git a/ec2api/tests/unit/test_ec2_validate.py b/ec2api/tests/unit/test_ec2_validate.py index fd977bf6..5f245259 100644 --- a/ec2api/tests/unit/test_ec2_validate.py +++ b/ec2api/tests/unit/test_ec2_validate.py @@ -76,6 +76,7 @@ class EC2ValidationTestCase(testtools.TestCase): validator.eipalloc_id('eipalloc-00000001') validator.eipassoc_id('eipassoc-00000001') validator.rtbassoc_id('rtbassoc-00000001') + validator.cgw_id('cgw-00000001') invalid_ids = ['1234', 'a-1111', '', 'i-1111', 'i-rrr', 'foobar'] @@ -97,6 +98,7 @@ class EC2ValidationTestCase(testtools.TestCase): check_raise_invalid_parameters(validator.eipalloc_id) check_raise_invalid_parameters(validator.eipassoc_id) check_raise_invalid_parameters(validator.rtbassoc_id) + check_raise_invalid_parameters(validator.cgw_id) invalid_ids = ['1234', 'a-1111', '', 'vpc-1111', 'vpc-rrr', 'foobar'] @@ -158,6 +160,16 @@ class EC2ValidationTestCase(testtools.TestCase): check_raise_validation_error('aa #^% -=99') check_raise_validation_error('x' * 256) + def test_validate_vpn_connection_type(self): + validator = common.Validator() + validator.vpn_connection_type('ipsec.1') + + invalid_ids = ['1234', 'a-1111', '', 'vpc-1111', 'vpc-rrr', 'foobar', + 'ipsec1', 'openvpn', 'pptp', 'l2tp', 'freelan'] + for id in invalid_ids: + self.assertRaises(exception.InvalidParameterValue, + validator.vpn_connection_type, id) + class EC2TimestampValidationTestCase(testtools.TestCase): """Test case for EC2 request timestamp validation.""" diff --git a/ec2api/tests/unit/test_ec2utils.py b/ec2api/tests/unit/test_ec2utils.py index 69229dc5..31f3564a 100644 --- a/ec2api/tests/unit/test_ec2utils.py +++ b/ec2api/tests/unit/test_ec2utils.py @@ -67,6 +67,7 @@ class EC2UtilsTestCase(testtools.TestCase): check_not_found('ami', exception.InvalidAMIIDNotFound) check_not_found('ari', exception.InvalidAMIIDNotFound) check_not_found('aki', exception.InvalidAMIIDNotFound) + check_not_found('cgw', exception.InvalidCustomerGatewayIDNotFound) @mock.patch('ec2api.db.api.IMPL') def test_get_db_items(self, db_api): @@ -121,6 +122,7 @@ class EC2UtilsTestCase(testtools.TestCase): check_not_found('ami', exception.InvalidAMIIDNotFound) check_not_found('aki', exception.InvalidAMIIDNotFound) check_not_found('ari', exception.InvalidAMIIDNotFound) + check_not_found('cgw', exception.InvalidCustomerGatewayIDNotFound) """Unit test api xml conversion.""" def test_number_conversion(self):