Handle OS errors for addresses, improve unit-tests

Change-Id: I12aad165c9f02a0d7efcf43ab94cff7809e7e7c6
This commit is contained in:
Feodor Tersin 2015-02-10 21:20:15 +03:00
parent 462de41470
commit c754004c21
3 changed files with 80 additions and 94 deletions

View File

@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import netaddr
try:
from neutronclient.common import exceptions as neutron_exception
except ImportError:
@ -47,15 +46,12 @@ def get_address_engine():
return AddressEngineNova()
# TODO(ft): generate unique association id
def allocate_address(context, domain=None):
if domain and domain not in ['vpc', 'standard']:
msg = _("Invalid value '%(domain)s' for domain.") % {'domain': domain}
raise exception.InvalidParameterValue(msg)
address, os_floating_ip = address_engine.allocate_address(context, domain)
return _format_address(context, address, os_floating_ip)
@ -142,15 +138,6 @@ class AddressDescriber(common.UniversalDescriber):
def describe_addresses(context, public_ip=None, allocation_id=None,
filter=None):
if public_ip:
for ip in public_ip:
try:
ip_address = netaddr.IPAddress(ip)
except Exception as ex:
raise exception.InvalidParameterValue(
value=str(public_ip),
parameter='public_ip',
reason='Invalid public IP specified')
formatted_addresses = AddressDescriber(
address_engine.get_os_ports(context)).describe(
context, allocation_id, public_ip, filter)
@ -166,7 +153,7 @@ def _format_address(context, address, os_floating_ip, os_ports=[]):
os_fip = os_floating_ip.get('instance_id')
if port_id:
port = next((port for port in os_ports
if port['id'] == port_id), None)
if port['id'] == port_id), None)
if port and port.get('device_id'):
ec2_address['instanceId'] = (
_get_instance_ec2_id_by_os_id(context, port['device_id']))
@ -227,9 +214,11 @@ class AddressEngineNeutron(object):
with common.OnCrashCleaner() as cleaner:
os_floating_ip = {'floating_network_id': os_public_network['id']}
# TODO(ft): handle error to process floating ip overlimit
os_floating_ip = neutron.create_floatingip(
{'floatingip': os_floating_ip})
try:
os_floating_ip = neutron.create_floatingip(
{'floatingip': os_floating_ip})
except neutron_exception.OverQuotaClient:
raise exception.AddressLimitExceeded()
os_floating_ip = os_floating_ip['floatingip']
cleaner.addCleanup(neutron.delete_floatingip, os_floating_ip['id'])
@ -243,7 +232,7 @@ class AddressEngineNeutron(object):
if public_ip:
# TODO(ft): implement search in DB layer
address = next((addr for addr in
db_api.get_items(context, 'eipalloc')
db_api.get_items(context, 'eipalloc')
if addr['public_ip'] == public_ip), None)
if address and _is_address_valid(context, neutron, address):
msg = _('You must specify an allocation id when releasing a '
@ -267,7 +256,6 @@ class AddressEngineNeutron(object):
try:
neutron.delete_floatingip(address['os_id'])
except neutron_exception.NotFound:
# TODO(ft): catch FloatingIPNotFound
pass
def associate_address(self, context, public_ip=None, instance_id=None,
@ -296,10 +284,13 @@ class AddressEngineNeutron(object):
# 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 them for EC2 Classic mode and raise an error.
return AddressEngineNova().associate_address(context,
public_ip=public_ip,
instance_id=instance_id)
# 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 instance_id:
if not instance_network_interfaces:
@ -330,7 +321,7 @@ class AddressEngineNeutron(object):
'associate-id %(eipassoc_id)s')
msg = msg % {'eipalloc_id': allocation_id,
'eipassoc_id': ec2utils.change_ec2_id_kind(
address['id'], 'eipassoc')}
address['id'], 'eipassoc')}
raise exception.ResourceAlreadyAssociated(msg)
else:
internet_gateways = (
@ -355,6 +346,7 @@ class AddressEngineNeutron(object):
'fixed_ip_address': private_ip_address}
neutron.update_floatingip(address['os_id'],
{'floatingip': os_floating_ip})
# TODO(ft): generate unique association id for each act of association
return ec2utils.change_ec2_id_kind(address['id'], 'eipassoc')
def disassociate_address(self, context, public_ip=None,
@ -369,8 +361,11 @@ class AddressEngineNeutron(object):
msg = _('You must specify an association id when unmapping '
'an address from a VPC instance')
raise exception.InvalidParameterValue(msg)
return AddressEngineNova().disassociate_address(context,
public_ip=public_ip)
# 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)
address = db_api.get_item_by_id(
context, 'eipalloc', ec2utils.change_ec2_id_kind(association_id,
@ -399,42 +394,41 @@ class AddressEngineNeutron(object):
class AddressEngineNova(object):
# TODO(ft): check that parameters unused in EC2 Classic mode are not
# specified
def allocate_address(self, context, domain=None):
nova = clients.nova(context)
nova_floating_ip = nova.floating_ips.create()
try:
nova_floating_ip = nova.floating_ips.create()
except nova_exception.Forbidden:
raise exception.AddressLimitExceeded()
return None, self.convert_ips_to_neutron_format(context,
[nova_floating_ip])[0]
def release_address(self, context, public_ip, allocation_id):
nova = clients.nova(context)
try:
nova.floating_ips.delete(self.get_ip_os_id(context, public_ip))
except nova_exception.NotFound:
# TODO(ft): catch FloatingIPNotFound
pass
nova.floating_ips.delete(self.get_nova_ip_by_public_ip(context,
public_ip).id)
def associate_address(self, context, public_ip=None, instance_id=None,
allocation_id=None, network_interface_id=None,
private_ip_address=None, allow_reassociation=False):
nova = clients.nova(context)
os_instance_id = ec2utils.get_db_item(context, 'i',
instance_id)['os_id']
# NOTE(ft): check the public IP exists to raise AWS exception otherwise
self.get_nova_ip_by_public_ip(context, public_ip)
nova = clients.nova(context)
nova.servers.add_floating_ip(os_instance_id, public_ip)
return None
def disassociate_address(self, context, public_ip=None,
association_id=None):
nova = clients.nova(context)
os_instance_id = self.get_nova_ip_by_public_ip(context,
public_ip).instance_id
if os_instance_id:
try:
nova.servers.remove_floating_ip(os_instance_id, public_ip)
except nova_exception.Forbidden:
raise exception.AuthFailure(
msg=_('The address %(public_ip)s does not belong to you') %
{'public_ip': public_ip})
nova = clients.nova(context)
nova.servers.remove_floating_ip(os_instance_id, public_ip)
return None
def get_os_floating_ips(self, context):
@ -454,21 +448,16 @@ class AddressEngineNova(object):
def get_os_ports(self, context):
return []
def get_ip_os_id(self, context, public_ip,
nova_floating_ips=None):
nova_ip = self.get_nova_ip_by_public_ip(context, public_ip,
nova_floating_ips)
return nova_ip.id
def get_nova_ip_by_public_ip(self, context, public_ip,
nova_floating_ips=None):
if nova_floating_ips is None:
nova = clients.nova(context)
nova_floating_ips = nova.floating_ips.list()
nova_ip = next((ip for ip in nova_floating_ips
if ip.ip == public_ip), None)
if ip.ip == public_ip), None)
if nova_ip is None:
raise exception.InvalidAddressNotFound(ip=public_ip)
msg = _("The address '%(public_ip)s' does not belong to you.")
raise exception.AuthFailure(msg % {'public_ip': public_ip})
return nova_ip

View File

@ -363,6 +363,10 @@ class SecurityGroupLimitExceeded(Overlimit):
msg_fmt = _('You have reached the limit of security groups')
class AddressLimitExceeded(Overlimit):
msg_fmt = _('The maximum number of addresses has been reached.')
class ImageNotActive(Invalid):
ec2_code = 'InvalidAMIID.Unavailable'
# TODO(ft): Change the message with the real AWS message

View File

@ -32,7 +32,7 @@ class AddressTestCase(base.ApiTestCase):
address.address_engine = (
address.AddressEngineNeutron())
self.nova_floating_ips.create.return_value = (
copy.deepcopy(fakes.NovaFloatingIp(fakes.NOVA_FLOATING_IP_1)))
fakes.NovaFloatingIp(fakes.NOVA_FLOATING_IP_1))
resp = self.execute('AllocateAddress', {})
self.assertEqual(200, resp['http_status_code'])
@ -82,16 +82,22 @@ class AddressTestCase(base.ApiTestCase):
self.assertEqual(0, self.db_api.add_item.call_count)
self.assertEqual(0, self.neutron.create_floatingip.call_count)
@base.skip_not_implemented
def test_allocate_address_overlimit(self):
address.address_engine = (
address.AddressEngineNeutron())
self.neutron.create_floatingip.side_effect = (
neutron_exception.OverQuotaClient())
resp = self.execute('AllocateAddress', {'Domain': 'vpc'})
self.assertEqual(400, resp['http_status_code'])
self.assertEqual('AddressLimitExceeded', resp['Error']['Code'])
address.address_engine = (
address.AddressEngineNeutron())
self.nova_floating_ips.create.side_effect = (
nova_exception.Forbidden(403))
resp = self.execute('AllocateAddress', {})
self.assertEqual(400, resp['http_status_code'])
self.assertEqual('AddressLimitExceeded', resp['Error']['Code'])
# AddressLimitExceeded
# standard - Too many addresses allocated
# vpc - The maximum number of addresses has been reached.
def test_allocate_address_vpc_rollback(self):
address.address_engine = (
@ -115,6 +121,9 @@ class AddressTestCase(base.ApiTestCase):
address.AddressEngineNeutron())
self.db_api.get_items.return_value = []
self.db_api.get_item_by_id.return_value = fakes.DB_INSTANCE_1
self.nova_floating_ips.list.return_value = (
[fakes.NovaFloatingIp(fakes.NOVA_FLOATING_IP_1),
fakes.NovaFloatingIp(fakes.NOVA_FLOATING_IP_2)])
self.nova_servers.add_floating_ip.return_value = True
resp = self.execute('AssociateAddress',
@ -259,10 +268,19 @@ class AddressTestCase(base.ApiTestCase):
self.assertEqual(400, resp['http_status_code'])
self.assertEqual('InvalidParameterCombination', resp['Error']['Code'])
# NOTE(ft): ec2 classic instance vs not existing public IP
self.db_api.get_item_by_id.return_value = fakes.DB_INSTANCE_1
self.nova_floating_ips.list.return_value = []
resp = self.execute('AssociateAddress',
{'PublicIp': fakes.IP_ADDRESS_1,
'InstanceId': fakes.ID_EC2_INSTANCE_2})
self.assertEqual(400, resp['http_status_code'])
self.assertEqual('AuthFailure', resp['Error']['Code'])
# NOTE(ft): ec2 classic instance vs vpc public ip
self.db_api.get_items.side_effect = (
lambda _, kind: [fakes.DB_ADDRESS_1, fakes.DB_ADDRESS_2]
if kind == 'eipalloc' else [])
self.db_api.get_items.side_effect = fakes.get_db_api_get_items(
{'eipalloc': [fakes.DB_ADDRESS_1, fakes.DB_ADDRESS_2],
'eni': []})
self.neutron.show_floatingip.return_value = (
{'floatingip': fakes.OS_FLOATING_IP_1})
resp = self.execute('AssociateAddress',
@ -353,27 +371,6 @@ class AddressTestCase(base.ApiTestCase):
'InstanceId': fakes.ID_EC2_INSTANCE_1},
'Gateway.NotAttached')
def test_associate_address_ec2_classic_broken_vpc(self):
address.address_engine = (
address.AddressEngineNeutron())
self.db_api.get_items.side_effect = (
lambda _, kind: [fakes.DB_ADDRESS_1, fakes.DB_ADDRESS_2]
if kind == 'eipalloc' else [])
self.neutron.show_floatingip.side_effect = neutron_exception.NotFound
self.db_api.get_item_by_id.return_value = fakes.DB_INSTANCE_2
self.nova_servers.add_floating_ip.return_value = True
resp = self.execute('AssociateAddress',
{'PublicIp': fakes.IP_ADDRESS_1,
'InstanceId': fakes.ID_EC2_INSTANCE_2})
self.assertEqual(200, resp['http_status_code'])
self.assertEqual(True, resp['return'])
self.assertNotIn('associationId', resp)
self.nova_servers.add_floating_ip.assert_called_once_with(
fakes.ID_OS_INSTANCE_2,
fakes.IP_ADDRESS_1)
def test_associate_address_vpc_rollback(self):
address.address_engine = (
address.AddressEngineNeutron())
@ -415,23 +412,6 @@ class AddressTestCase(base.ApiTestCase):
self.assertEqual(True, resp['return'])
self.assertEqual(1, self.nova_servers.remove_floating_ip.call_count)
def test_dissassociate_address_ec2_classic_invalid(self):
address.address_engine = (
address.AddressEngineNeutron())
self.db_api.get_items.return_value = []
self.nova_servers.remove_floating_ip.side_effect = (
nova_exception.Forbidden(403))
self.nova_floating_ips.list.return_value = (
[fakes.NovaFloatingIp(fakes.NOVA_FLOATING_IP_1),
fakes.NovaFloatingIp(fakes.NOVA_FLOATING_IP_2)])
resp = self.execute('DisassociateAddress',
{'PublicIp': fakes.IP_ADDRESS_2})
self.assertEqual(400, resp['http_status_code'])
self.assertEqual('AuthFailure', resp['Error']['Code'])
self.nova_servers.remove_floating_ip.assert_called_once_with(
fakes.ID_OS_INSTANCE_1,
fakes.IP_ADDRESS_2)
def test_dissassociate_address_vpc(self):
address.address_engine = (
address.AddressEngineNeutron())
@ -485,6 +465,14 @@ class AddressTestCase(base.ApiTestCase):
'AssociationId': 'eipassoc-0'},
'InvalidParameterCombination')
# NOTE(ft): EC2 Classic public IP does not exists
self.db_api.get_items.return_value = []
self.nova_floating_ips.list.return_value = []
resp = self.execute('DisassociateAddress',
{'PublicIp': fakes.IP_ADDRESS_2})
self.assertEqual(400, resp['http_status_code'])
self.assertEqual('AuthFailure', resp['Error']['Code'])
# NOTE(ft): vpc address vs public ip parameter
self.db_api.get_items.return_value = [fakes.DB_ADDRESS_1]
self.neutron.show_floatingip.return_value = (
@ -568,6 +556,11 @@ class AddressTestCase(base.ApiTestCase):
'AllocationId': 'eipalloc-0'},
'InvalidParameterCombination')
# NOTE(ft): EC2 Classic public IP is not found
self.nova_floating_ips.list.return_value = []
do_check({'PublicIp': fakes.IP_ADDRESS_1},
'AuthFailure')
# NOTE(ft): vpc address vs public ip parameter
self.db_api.get_items.return_value = [fakes.DB_ADDRESS_1]
self.neutron.show_floatingip.return_value = (