Handle OS errors for addresses, improve unit-tests
Change-Id: I12aad165c9f02a0d7efcf43ab94cff7809e7e7c6
This commit is contained in:
parent
462de41470
commit
c754004c21
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 = (
|
||||
|
Loading…
Reference in New Issue
Block a user