diff --git a/ec2api/api/address.py b/ec2api/api/address.py index 3fb74dbb..d8aee325 100644 --- a/ec2api/api/address.py +++ b/ec2api/api/address.py @@ -210,7 +210,8 @@ def _disassociate_address_item(context, address): class AddressEngineNeutron(object): def allocate_address(self, context, domain=None): - if not domain or domain == 'standard': + if ((not domain or domain == 'standard') and + not CONF.disable_ec2_classic): return AddressEngineNova().allocate_address(context) os_public_network = ec2utils.get_os_public_network(context) neutron = clients.neutron(context) @@ -248,9 +249,27 @@ class AddressEngineNeutron(object): if not _is_address_valid(context, neutron, address): raise exception.InvalidAllocationIDNotFound( id=allocation_id) + if 'network_interface_id' in address: - raise exception.InvalidIPAddressInUse( - ip_address=address['public_ip']) + if CONF.disable_ec2_classic: + network_interface_id = address['network_interface_id'] + network_interface = db_api.get_item_by_id(context, + network_interface_id) + default_vpc = ec2utils.check_and_create_default_vpc(context) + if default_vpc: + default_vpc_id = default_vpc['id'] + if (network_interface and + network_interface['vpc_id'] == default_vpc_id): + association_id = ec2utils.change_ec2_id_kind(address['id'], + 'eipassoc') + self.disassociate_address( + context, association_id=association_id) + else: + raise exception.InvalidIPAddressInUse( + ip_address=address['public_ip']) + else: + raise exception.InvalidIPAddressInUse( + ip_address=address['public_ip']) with common.OnCrashCleaner() as cleaner: db_api.delete_item(context, address['id']) @@ -272,28 +291,37 @@ class AddressEngineNeutron(object): instance_network_interfaces.append(eni) neutron = clients.neutron(context) + if public_ip: - if instance_network_interfaces: - msg = _('You must specify an allocation id when mapping ' - 'an address to a VPC instance') - raise exception.InvalidParameterCombination(msg) # TODO(ft): implement search in DB layer address = next((addr for addr in db_api.get_items(context, 'eipalloc') if addr['public_ip'] == public_ip), None) - if address and _is_address_valid(context, neutron, address): + + if not CONF.disable_ec2_classic: + if instance_network_interfaces: + msg = _('You must specify an allocation id when mapping ' + 'an address to a VPC instance') + raise exception.InvalidParameterCombination(msg) + if address and _is_address_valid(context, neutron, address): + msg = _( + "The address '%(public_ip)s' does not belong to you.") + raise exception.AuthFailure(msg % {'public_ip': public_ip}) + + # NOTE(ft): in fact only the first two parameters are used to + # associate an address in EC2 Classic mode. Other parameters + # are sent to validate their emptiness in one place + return AddressEngineNova().associate_address( + context, public_ip=public_ip, instance_id=instance_id, + allocation_id=allocation_id, + network_interface_id=network_interface_id, + private_ip_address=private_ip_address, + allow_reassociation=allow_reassociation) + + if not address: msg = _("The address '%(public_ip)s' does not belong to you.") raise exception.AuthFailure(msg % {'public_ip': public_ip}) - - # NOTE(ft): in fact only the first two parameters are used to - # associate an address in EC2 Classic mode. Other parameters are - # sent to validate their emptiness in one place - return AddressEngineNova().associate_address( - context, public_ip=public_ip, instance_id=instance_id, - allocation_id=allocation_id, - network_interface_id=network_interface_id, - private_ip_address=private_ip_address, - allow_reassociation=allow_reassociation) + allocation_id = address['id'] if instance_id: if not instance_network_interfaces: @@ -355,20 +383,33 @@ class AddressEngineNeutron(object): def disassociate_address(self, context, public_ip=None, association_id=None): neutron = clients.neutron(context) + if public_ip: # TODO(ft): implement search in DB layer address = next((addr for addr in db_api.get_items(context, 'eipalloc') if addr['public_ip'] == public_ip), None) - if address and _is_address_valid(context, neutron, address): + + if not CONF.disable_ec2_classic: + if address and _is_address_valid(context, neutron, address): + msg = _('You must specify an association id when ' + 'unmapping an address from a VPC instance') + raise exception.InvalidParameterValue(msg) + # NOTE(ft): association_id is unused in EC2 Classic mode, + # but it's passed there to validate its emptiness in one place + return AddressEngineNova().disassociate_address( + context, public_ip=public_ip, + association_id=association_id) + + if not address: + msg = _("The address '%(public_ip)s' does not belong to you.") + raise exception.AuthFailure(msg % {'public_ip': public_ip}) + if 'network_interface_id' not in address: msg = _('You must specify an association id when unmapping ' 'an address from a VPC instance') raise exception.InvalidParameterValue(msg) - # NOTE(ft): association_id is unused in EC2 Classic mode, but it's - # passed there to validate its emptiness in one place - return AddressEngineNova().disassociate_address( - context, public_ip=public_ip, - association_id=association_id) + association_id = ec2utils.change_ec2_id_kind(address['id'], + 'eipassoc') address = db_api.get_item_by_id( context, ec2utils.change_ec2_id_kind(association_id, 'eipalloc')) diff --git a/ec2api/tests/unit/fakes.py b/ec2api/tests/unit/fakes.py index 807a6767..dec7b4b8 100644 --- a/ec2api/tests/unit/fakes.py +++ b/ec2api/tests/unit/fakes.py @@ -145,8 +145,11 @@ ID_EC2_DHCP_OPTIONS_2 = random_ec2_id('dopt') # address constants +ID_EC2_ADDRESS_DEFAULT = random_ec2_id('eipalloc') ID_EC2_ADDRESS_1 = random_ec2_id('eipalloc') ID_EC2_ADDRESS_2 = random_ec2_id('eipalloc') +ID_EC2_ASSOCIATION_DEFAULT = ID_EC2_ADDRESS_DEFAULT.replace('eipalloc', + 'eipassoc') ID_EC2_ASSOCIATION_1 = ID_EC2_ADDRESS_1.replace('eipalloc', 'eipassoc') ID_EC2_ASSOCIATION_2 = ID_EC2_ADDRESS_2.replace('eipalloc', 'eipassoc') ID_OS_FLOATING_IP_1 = random_os_id() @@ -1043,6 +1046,14 @@ class NovaFloatingIp(object): self.fixed_ip = nova_ip_dict['fixed_ip'] self.instance_id = nova_ip_dict['instance_id'] +DB_ADDRESS_DEFAULT = { + 'id': ID_EC2_ADDRESS_DEFAULT, + 'os_id': ID_OS_FLOATING_IP_2, + 'vpc_id': None, + 'public_ip': IP_ADDRESS_2, + 'network_interface_id': ID_EC2_NETWORK_INTERFACE_DEFAULT, + 'private_ip_address': IP_NETWORK_INTERFACE_DEFAULT, +} DB_ADDRESS_1 = { 'id': ID_EC2_ADDRESS_1, 'os_id': ID_OS_FLOATING_IP_1, @@ -1083,6 +1094,16 @@ EC2_ADDRESS_2 = { 'privateIpAddress': IP_NETWORK_INTERFACE_2, 'networkInterfaceOwnerId': ID_OS_PROJECT, } +EC2_ADDRESS_DEFAULT = { + 'allocationId': ID_EC2_ADDRESS_DEFAULT, + 'publicIp': IP_ADDRESS_2, + 'domain': 'vpc', + 'instanceId': ID_EC2_INSTANCE_DEFAULT, + 'associationId': ID_EC2_ASSOCIATION_DEFAULT, + 'networkInterfaceId': ID_EC2_NETWORK_INTERFACE_DEFAULT, + 'privateIpAddress': IP_NETWORK_INTERFACE_DEFAULT, + 'networkInterfaceOwnerId': ID_OS_PROJECT, +} OS_FLOATING_IP_1 = { 'id': ID_OS_FLOATING_IP_1, diff --git a/ec2api/tests/unit/test_address.py b/ec2api/tests/unit/test_address.py index cf3872db..59d9a3c5 100644 --- a/ec2api/tests/unit/test_address.py +++ b/ec2api/tests/unit/test_address.py @@ -57,6 +57,28 @@ class AddressTestCase(base.ApiTestCase): resp = self.execute('AllocateAddress', {'Domain': 'vpc'}) + self.assertEqual(fakes.IP_ADDRESS_1, resp['publicIp']) + self.assertEqual('vpc', resp['domain']) + self.assertEqual(fakes.ID_EC2_ADDRESS_1, + resp['allocationId']) + self.db_api.add_item.assert_called_once_with( + mock.ANY, 'eipalloc', + tools.purge_dict(fakes.DB_ADDRESS_1, + ('id', 'vpc_id'))) + self.neutron.create_floatingip.assert_called_once_with( + {'floatingip': { + 'floating_network_id': + fakes.ID_OS_PUBLIC_NETWORK}}) + self.neutron.list_networks.assert_called_once_with( + **{'router:external': True, + 'name': fakes.NAME_OS_PUBLIC_NETWORK}) + self.db_api.reset_mock() + self.neutron.create_floatingip.reset_mock() + self.neutron.list_networks.reset_mock() + + self.configure(disable_ec2_classic=True) + resp = self.execute('AllocateAddress', {}) + self.assertEqual(fakes.IP_ADDRESS_1, resp['publicIp']) self.assertEqual('vpc', resp['domain']) self.assertEqual(fakes.ID_EC2_ADDRESS_1, @@ -192,6 +214,14 @@ class AddressTestCase(base.ApiTestCase): 'AllowReassociation': 'True'}, fakes.IP_NETWORK_INTERFACE_2) + self.configure(disable_ec2_classic=True) + self.set_mock_db_items( + fakes.DB_VPC_DEFAULT, fakes.DB_ADDRESS_1, fakes.DB_IGW_1, + fakes.DB_NETWORK_INTERFACE_2) + do_check({'PublicIp': fakes.IP_ADDRESS_1, + 'InstanceId': fakes.ID_EC2_INSTANCE_1}, + fakes.IP_NETWORK_INTERFACE_2) + def test_associate_address_vpc_idempotent(self): address.address_engine = ( address.AddressEngineNeutron()) @@ -338,6 +368,17 @@ class AddressTestCase(base.ApiTestCase): {'AllocationId': fakes.ID_EC2_ADDRESS_1, 'InstanceId': fakes.ID_EC2_INSTANCE_1}) + # NOTE(tikitavi): associate to wrong public ip + self.configure(disable_ec2_classic=True) + self.set_mock_db_items( + fakes.DB_VPC_DEFAULT, fakes.DB_IGW_DEFAULT, fakes.DB_ADDRESS_1, + fakes.DB_INSTANCE_DEFAULT, tools.update_dict( + fakes.DB_NETWORK_INTERFACE_DEFAULT, + {'instance_id': fakes.ID_EC2_INSTANCE_DEFAULT})) + do_check({'PublicIp': '0.0.0.0', + 'InstanceId': fakes.ID_EC2_INSTANCE_DEFAULT}, + 'AuthFailure') + @tools.screen_unexpected_exception_logs def test_associate_address_vpc_rollback(self): address.address_engine = ( @@ -388,6 +429,22 @@ class AddressTestCase(base.ApiTestCase): {'AssociationId': fakes.ID_EC2_ASSOCIATION_2}) self.assertEqual(True, resp['return']) + self.neutron.update_floatingip.assert_called_once_with( + fakes.ID_OS_FLOATING_IP_2, + {'floatingip': {'port_id': None}}) + self.db_api.update_item.assert_called_once_with( + mock.ANY, + tools.purge_dict(fakes.DB_ADDRESS_2, ['network_interface_id', + 'private_ip_address'])) + self.neutron.update_floatingip.reset_mock() + self.db_api.update_item.reset_mock() + + self.configure(disable_ec2_classic=True) + + resp = self.execute('DisassociateAddress', + {'PublicIp': fakes.IP_ADDRESS_2}) + self.assertEqual(True, resp['return']) + self.neutron.update_floatingip.assert_called_once_with( fakes.ID_OS_FLOATING_IP_2, {'floatingip': {'port_id': None}}) @@ -448,6 +505,18 @@ class AddressTestCase(base.ApiTestCase): do_check({'AssociationId': fakes.ID_EC2_ASSOCIATION_2}, 'InvalidAssociationID.NotFound') + # NOTE(tikitavi): disassociate to wrong public ip + self.configure(disable_ec2_classic=True) + self.set_mock_db_items() + self.assert_execution_error('AuthFailure', 'DisassociateAddress', + {'PublicIp': fakes.IP_ADDRESS_2}) + + # NOTE(tikitavi): disassociate to unassociated ip + self.set_mock_db_items(fakes.DB_ADDRESS_1) + self.assert_execution_error('InvalidParameterValue', + 'DisassociateAddress', + {'PublicIp': fakes.IP_ADDRESS_1}) + @tools.screen_unexpected_exception_logs def test_dissassociate_address_vpc_rollback(self): address.address_engine = ( @@ -496,6 +565,28 @@ class AddressTestCase(base.ApiTestCase): self.db_api.delete_item.assert_called_once_with( mock.ANY, fakes.ID_EC2_ADDRESS_1) + @mock.patch('ec2api.api.address.AddressEngineNeutron.disassociate_address') + def test_release_address_default_vpc(self, disassociate_address): + address.address_engine = ( + address.AddressEngineNeutron()) + self.configure(disable_ec2_classic=True) + self.set_mock_db_items(fakes.DB_VPC_DEFAULT, + fakes.DB_ADDRESS_DEFAULT, + fakes.DB_NETWORK_INTERFACE_DEFAULT) + self.neutron.show_floatingip.return_value = ( + {'floatingip': fakes.OS_FLOATING_IP_2}) + + resp = self.execute('ReleaseAddress', + {'AllocationId': fakes.ID_EC2_ADDRESS_DEFAULT}) + self.assertEqual(True, resp['return']) + + disassociate_address.assert_called_once_with( + mock.ANY, association_id=fakes.ID_EC2_ASSOCIATION_DEFAULT) + self.neutron.delete_floatingip.assert_called_once_with( + fakes.ID_OS_FLOATING_IP_2) + self.db_api.delete_item.assert_called_once_with( + mock.ANY, fakes.ID_EC2_ADDRESS_DEFAULT) + def test_release_address_invalid_parameters(self): address.address_engine = ( address.AddressEngineNeutron()) @@ -541,6 +632,18 @@ class AddressTestCase(base.ApiTestCase): do_check({'AllocationId': fakes.ID_EC2_ADDRESS_2}, 'InvalidIPAddress.InUse') + # NOTE(tikitavi): address is in use in not default vpc + self.configure(disable_ec2_classic=True) + self.set_mock_db_items(fakes.DB_VPC_DEFAULT, + fakes.DB_VPC_1, + fakes.DB_ADDRESS_2, + fakes.DB_NETWORK_INTERFACE_2) + self.neutron.show_floatingip.return_value = ( + {'floatingip': fakes.OS_FLOATING_IP_2}) + + do_check({'AllocationId': fakes.ID_EC2_ADDRESS_2}, + 'InvalidIPAddress.InUse') + @tools.screen_unexpected_exception_logs def test_release_address_vpc_rollback(self): address.address_engine = (