diff --git a/openstack_dashboard/api/__init__.py b/openstack_dashboard/api/__init__.py index aca05cf43..06dced72c 100644 --- a/openstack_dashboard/api/__init__.py +++ b/openstack_dashboard/api/__init__.py @@ -36,6 +36,7 @@ from openstack_dashboard.api import base from openstack_dashboard.api import cinder from openstack_dashboard.api import glance from openstack_dashboard.api import keystone +from openstack_dashboard.api import network from openstack_dashboard.api import nova from openstack_dashboard.api import quantum from openstack_dashboard.api import swift diff --git a/openstack_dashboard/api/network.py b/openstack_dashboard/api/network.py new file mode 100644 index 000000000..a7e28ba6b --- /dev/null +++ b/openstack_dashboard/api/network.py @@ -0,0 +1,171 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 NEC Corporation +# +# 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. + +"""Abstraction layer of networking functionalities. + +Now Nova and Quantum have duplicated features. +Thie API layer is introduced to hide the differences between them +from dashboard implementations. +""" + +import abc + + +class NetworkClient(object): + def __init__(self, request): + from openstack_dashboard import api + if api.base.is_service_enabled(request, 'network'): + self.floating_ips = api.quantum.FloatingIpManager(request) + else: + self.floating_ips = api.nova.FloatingIpManager(request) + + +class FloatingIpManager(object): + """Abstract class to implement Floating IP methods + + FloatingIP object returned from methods in this class + must contains the following attributes: + - id : ID of Floating IP + - ip : Floating IP address + - pool : ID of Floating IP pool from which the address is allocated + - fixed_ip : Fixed IP address of a VIF associated with the address + - port_id : ID of a VIF associated with the address + (instance_id when Nova floating IP is used) + - instance_id : Instance ID of an associated with the Floating IP +""" + + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def list_pools(self): + """Fetches a list of all floating IP pools. + + A list of FloatingIpPool object is returned. + FloatingIpPool object is an APIResourceWrapper/APIDictWrapper + where 'id' and 'name' attributes are defined. + """ + pass + + @abc.abstractmethod + def list(self): + """Fetches a list all floating IPs. + + A returned value is a list of FloatingIp object. + """ + pass + + @abc.abstractmethod + def get(self, floating_ip_id): + """Fetches the floating IP. + + It returns a FloatingIp object corresponding to floating_ip_id. + """ + pass + + @abc.abstractmethod + def allocate(self, pool=None): + """Allocates a floating IP to the tenant. + + You must provide a pool name or id for which you would like to + allocate an floating IP. + """ + pass + + @abc.abstractmethod + def release(self, floating_ip_id): + """Releases a floating IP specified.""" + pass + + @abc.abstractmethod + def associate(self, floating_ip_id, port_id): + """Associates the floating IP to the port. + + port_id is a fixed IP of a instance (Nova) or + a port_id attached to a VNIC of a instance. + """ + pass + + @abc.abstractmethod + def disassociate(self, floating_ip_id, port_id): + """Disassociates the floating IP from the port. + + port_id is a fixed IP of a instance (Nova) or + a port_id attached to a VNIC of a instance. + """ + pass + + @abc.abstractmethod + def list_targets(self): + """Returns a list of association targets of instance VIFs. + + Each association target is represented as FloatingIpTarget object. + FloatingIpTarget is a APIResourceWrapper/APIDictWrapper and + 'id' and 'name' attributes must be defined in each object. + FloatingIpTarget.id can be passed as port_id in associate(). + FloatingIpTarget.name is displayed in Floating Ip Association Form. + """ + pass + + @abc.abstractmethod + def get_target_id_by_instance(self, instance_id): + """Returns a target ID of floating IP association based on + a backend implementation. + """ + pass + + @abc.abstractmethod + def is_simple_associate_supported(self): + """Returns True if the default floating IP pool is enabled.""" + pass + + +def floating_ip_pools_list(request): + return NetworkClient(request).floating_ips.list_pools() + + +def tenant_floating_ip_list(request): + return NetworkClient(request).floating_ips.list() + + +def tenant_floating_ip_get(request, floating_ip_id): + return NetworkClient(request).floating_ips.get(floating_ip_id) + + +def tenant_floating_ip_allocate(request, pool=None): + return NetworkClient(request).floating_ips.allocate(pool) + + +def tenant_floating_ip_release(request, floating_ip_id): + return NetworkClient(request).floating_ips.release(floating_ip_id) + + +def floating_ip_associate(request, floating_ip_id, port_id): + return NetworkClient(request).floating_ips.associate(floating_ip_id, + port_id) + + +def floating_ip_disassociate(request, floating_ip_id, port_id): + return NetworkClient(request).floating_ips.disassociate(floating_ip_id, + port_id) + + +def floating_ip_target_list(request): + return NetworkClient(request).floating_ips.list_targets() + + +def floating_ip_target_get_by_instance(request, instance_id): + return NetworkClient(request).floating_ips.get_target_id_by_instance( + instance_id) diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 6c94b923f..d1fa1ee2c 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -32,10 +32,12 @@ from novaclient.v1_1 import security_group_rules as nova_rules from novaclient.v1_1.security_groups import SecurityGroup as NovaSecurityGroup from novaclient.v1_1.servers import REBOOT_HARD +from horizon.conf import HORIZON_CONFIG from horizon.utils.memoized import memoized from openstack_dashboard.api.base import (APIResourceWrapper, QuotaSet, APIDictWrapper, url_for) +from openstack_dashboard.api import network LOG = logging.getLogger(__name__) @@ -182,6 +184,71 @@ class FlavorExtraSpec(object): self.value = val +class FloatingIp(APIResourceWrapper): + _attrs = ['id', 'ip', 'fixed_ip', 'port_id', 'instance_id', 'pool'] + + def __init__(self, fip): + fip.__setattr__('port_id', fip.instance_id) + super(FloatingIp, self).__init__(fip) + + +class FloatingIpPool(APIDictWrapper): + def __init__(self, pool): + pool_dict = {'id': pool.name, + 'name': pool.name} + super(FloatingIpPool, self).__init__(pool_dict) + + +class FloatingIpTarget(APIDictWrapper): + def __init__(self, server): + server_dict = {'name': '%s (%s)' % (server.name, server.id), + 'id': server.id} + super(FloatingIpTarget, self).__init__(server_dict) + + +class FloatingIpManager(network.FloatingIpManager): + def __init__(self, request): + self.request = request + self.client = novaclient(request) + + def list_pools(self): + return [FloatingIpPool(pool) + for pool in self.client.floating_ip_pools.list()] + + def list(self): + return [FloatingIp(fip) + for fip in self.client.floating_ips.list()] + + def get(self, floating_ip_id): + return FloatingIp(self.client.floating_ips.get(floating_ip_id)) + + def allocate(self, pool): + return FloatingIp(self.client.floating_ips.create(pool=pool)) + + def release(self, floating_ip_id): + self.client.floating_ips.delete(floating_ip_id) + + def associate(self, floating_ip_id, port_id): + # In Nova implied port_id is instance_id + server = self.client.servers.get(port_id) + fip = self.client.floating_ips.get(floating_ip_id) + self.client.servers.add_floating_ip(server.id, fip.ip) + + def disassociate(self, floating_ip_id, port_id): + fip = self.client.floating_ips.get(floating_ip_id) + server = self.client.servers.get(fip.instance_id) + self.client.servers.remove_floating_ip(server.id, fip.ip) + + def list_targets(self): + return [FloatingIpTarget(s) for s in self.client.servers.list()] + + def get_target_id_by_instance(self, instance_id): + return instance_id + + def is_simple_associate_supported(self): + return HORIZON_CONFIG["simple_ip_management"] + + def novaclient(request): insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) LOG.debug('novaclient connection created using token "%s" and url "%s"' % @@ -255,33 +322,6 @@ def flavor_extra_set(request, flavor_id, metadata): return flavor.set_keys(metadata) -def tenant_floating_ip_list(request): - """Fetches a list of all floating ips.""" - return novaclient(request).floating_ips.list() - - -def floating_ip_pools_list(request): - """Fetches a list of all floating ip pools.""" - return novaclient(request).floating_ip_pools.list() - - -def tenant_floating_ip_get(request, floating_ip_id): - """Fetches a floating ip.""" - return novaclient(request).floating_ips.get(floating_ip_id) - - -def tenant_floating_ip_allocate(request, pool=None): - """Allocates a floating ip to tenant. Optionally you may provide a pool - for which you would like the IP. - """ - return novaclient(request).floating_ips.create(pool=pool) - - -def tenant_floating_ip_release(request, floating_ip_id): - """Releases floating ip from the pool of a tenant.""" - return novaclient(request).floating_ips.delete(floating_ip_id) - - def snapshot_create(request, instance_id, name): return novaclient(request).servers.create_image(instance_id, name) @@ -402,22 +442,6 @@ def server_revert_resize(request, instance_id): novaclient(request).servers.revert_resize(instance_id) -def server_add_floating_ip(request, server, floating_ip): - """Associates floating IP to server's fixed IP. - """ - server = novaclient(request).servers.get(server) - fip = novaclient(request).floating_ips.get(floating_ip) - return novaclient(request).servers.add_floating_ip(server.id, fip.ip) - - -def server_remove_floating_ip(request, server, floating_ip): - """Removes relationship between floating and server's fixed ip. - """ - fip = novaclient(request).floating_ips.get(floating_ip) - server = novaclient(request).servers.get(fip.instance_id) - return novaclient(request).servers.remove_floating_ip(server.id, fip.ip) - - def tenant_quota_get(request, tenant_id): return QuotaSet(novaclient(request).quotas.get(tenant_id)) diff --git a/openstack_dashboard/api/quantum.py b/openstack_dashboard/api/quantum.py index c41309bfc..3e23cb810 100644 --- a/openstack_dashboard/api/quantum.py +++ b/openstack_dashboard/api/quantum.py @@ -26,11 +26,17 @@ import logging from quantumclient.v2_0 import client as quantum_client from django.utils.datastructures import SortedDict +from horizon.conf import HORIZON_CONFIG + from openstack_dashboard.api.base import APIDictWrapper, url_for +from openstack_dashboard.api import network +from openstack_dashboard.api import nova LOG = logging.getLogger(__name__) +IP_VERSION_DICT = {4: 'IPv4', 6: 'IPv6'} + class QuantumAPIDictWrapper(APIDictWrapper): @@ -87,7 +93,116 @@ class Router(QuantumAPIDictWrapper): super(Router, self).__init__(apiresource) -IP_VERSION_DICT = {4: 'IPv4', 6: 'IPv6'} +class FloatingIp(APIDictWrapper): + _attrs = ['id', 'ip', 'fixed_ip', 'port_id', 'instance_id', 'pool'] + + def __init__(self, fip): + fip['ip'] = fip['floating_ip_address'] + fip['fixed_ip'] = fip['fixed_ip_address'] + fip['pool'] = fip['floating_network_id'] + super(FloatingIp, self).__init__(fip) + + +class FloatingIpPool(APIDictWrapper): + pass + + +class FloatingIpTarget(APIDictWrapper): + pass + + +class FloatingIpManager(network.FloatingIpManager): + def __init__(self, request): + self.request = request + self.client = quantumclient(request) + + def list_pools(self): + search_opts = {'router:external': True} + return [FloatingIpPool(pool) for pool + in self.client.list_networks(**search_opts).get('networks')] + + def list(self): + fips = self.client.list_floatingips().get('floatingips') + # Get port list to add instance_id to floating IP list + # instance_id is stored in device_id attribute + ports = port_list(self.request) + device_id_dict = SortedDict([(p['id'], p['device_id']) for p in ports]) + for fip in fips: + if fip['port_id']: + fip['instance_id'] = device_id_dict[fip['port_id']] + else: + fip['instance_id'] = None + return [FloatingIp(fip) for fip in fips] + + def get(self, floating_ip_id): + fip = self.client.show_floatingip(floating_ip_id).get('floatingip') + if fip['port_id']: + fip['instance_id'] = port_get(self.request, + fip['port_id']).device_id + else: + fip['instance_id'] = None + return FloatingIp(fip) + + def allocate(self, pool): + body = {'floatingip': {'floating_network_id': pool}} + fip = self.client.create_floatingip(body).get('floatingip') + fip['instance_id'] = None + return FloatingIp(fip) + + def release(self, floating_ip_id): + self.client.delete_floatingip(floating_ip_id) + + def associate(self, floating_ip_id, port_id): + # NOTE: In Quantum Horizon floating IP support, port_id is + # "_" format to identify multiple ports. + pid, ip_address = port_id.split('_', 1) + update_dict = {'port_id': pid, + 'fixed_ip_address': ip_address} + self.client.update_floatingip(floating_ip_id, + {'floatingip': update_dict}) + + def disassociate(self, floating_ip_id, port_id): + update_dict = {'port_id': None} + self.client.update_floatingip(floating_ip_id, + {'floatingip': update_dict}) + + def list_targets(self): + ports = port_list(self.request) + servers = nova.server_list(self.request) + server_dict = SortedDict([(s.id, s.name) for s in servers]) + targets = [] + for p in ports: + # Remove network ports from Floating IP targets + if p.device_owner.startswith('network:'): + continue + port_id = p.id + server_name = server_dict.get(p.device_id) + for ip in p.fixed_ips: + target = {'name': '%s: %s' % (server_name, ip['ip_address']), + 'id': '%s_%s' % (port_id, ip['ip_address'])} + targets.append(FloatingIpTarget(target)) + return targets + + def get_target_id_by_instance(self, instance_id): + # In Quantum one port can have multiple ip addresses, so this method + # picks up the first one and generate target id. + if not instance_id: + return None + search_opts = {'device_id': instance_id} + ports = port_list(self.request, **search_opts) + if not ports: + return None + return '%s_%s' % (ports[0].id, ports[0].fixed_ips[0]['ip_address']) + + def is_simple_associate_supported(self): + # NOTE: There are two reason that simple association support + # needs more considerations. (1) Quantum does not support the + # default floating IP pool at the moment. It can be avoided + # in case where only one floating IP pool exists. + # (2) Quantum floating IP is associated with each VIF and + # we need to check whether such VIF is only one for an instance + # to enable simple association support. + return False def get_ipver_str(ip_version): diff --git a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/forms.py b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/forms.py index 254256991..40918f787 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/forms.py +++ b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/forms.py @@ -38,7 +38,7 @@ class FloatingIpAllocate(forms.SelfHandlingForm): def handle(self, request, data): try: - fip = api.nova.tenant_floating_ip_allocate(request, + fip = api.network.tenant_floating_ip_allocate(request, pool=data['pool']) messages.success(request, _('Allocated Floating IP %(ip)s.') diff --git a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tables.py b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tables.py index 1124b05c8..5dc5e4943 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tables.py +++ b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tables.py @@ -53,7 +53,7 @@ class ReleaseIPs(tables.BatchAction): classes = ('btn-danger', 'btn-release') def action(self, request, obj_id): - api.nova.tenant_floating_ip_release(request, obj_id) + api.network.tenant_floating_ip_release(request, obj_id) class AssociateIP(tables.LinkAction): @@ -63,7 +63,7 @@ class AssociateIP(tables.LinkAction): classes = ("ajax-modal", "btn-associate") def allowed(self, request, fip): - if fip.instance_id: + if fip.port_id: return False return True @@ -79,15 +79,15 @@ class DisassociateIP(tables.Action): classes = ("btn-disassociate", "btn-danger") def allowed(self, request, fip): - if fip.instance_id: + if fip.port_id: return True return False def single(self, table, request, obj_id): try: fip = table.get_object_by_id(get_int_or_uuid(obj_id)) - api.nova.server_remove_floating_ip(request, fip.instance_id, - fip.id) + api.network.floating_ip_disassociate(request, fip.id, + fip.port_id) LOG.info('Disassociating Floating IP "%s".' % obj_id) messages.success(request, _('Successfully disassociated Floating IP: %s') @@ -116,7 +116,7 @@ class FloatingIPsTable(tables.DataTable): link=get_instance_link, verbose_name=_("Instance"), empty_value="-") - pool = tables.Column("pool", + pool = tables.Column("pool_name", verbose_name=_("Floating IP Pool"), empty_value="-") diff --git a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py index ef9180b5f..004681f11 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py +++ b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py @@ -38,11 +38,11 @@ NAMESPACE = "horizon:project:access_and_security:floating_ips" class FloatingIpViewTests(test.TestCase): def test_associate(self): - self.mox.StubOutWithMock(api.nova, 'server_list') - self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_list') - api.nova.server_list(IsA(http.HttpRequest)) \ + self.mox.StubOutWithMock(api.network, 'floating_ip_target_list') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') + api.network.floating_ip_target_list(IsA(http.HttpRequest)) \ .AndReturn(self.servers.list()) - api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ .AndReturn(self.floating_ips.list()) self.mox.ReplayAll() @@ -58,17 +58,17 @@ class FloatingIpViewTests(test.TestCase): def test_associate_post(self): floating_ip = self.floating_ips.list()[1] server = self.servers.first() - self.mox.StubOutWithMock(api.nova, 'server_add_floating_ip') - self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_list') - self.mox.StubOutWithMock(api.nova, 'server_list') + self.mox.StubOutWithMock(api.network, 'floating_ip_associate') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') + self.mox.StubOutWithMock(api.network, 'floating_ip_target_list') - api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ .AndReturn(self.floating_ips.list()) - api.nova.server_list(IsA(http.HttpRequest)) \ + api.network.floating_ip_target_list(IsA(http.HttpRequest)) \ .AndReturn(self.servers.list()) - api.nova.server_add_floating_ip(IsA(http.HttpRequest), - server.id, - floating_ip.id) + api.network.floating_ip_associate(IsA(http.HttpRequest), + floating_ip.id, + server.id) self.mox.ReplayAll() form_data = {'instance_id': server.id, @@ -80,17 +80,17 @@ class FloatingIpViewTests(test.TestCase): def test_associate_post_with_redirect(self): floating_ip = self.floating_ips.list()[1] server = self.servers.first() - self.mox.StubOutWithMock(api.nova, 'server_add_floating_ip') - self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_list') - self.mox.StubOutWithMock(api.nova, 'server_list') + self.mox.StubOutWithMock(api.network, 'floating_ip_associate') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') + self.mox.StubOutWithMock(api.network, 'floating_ip_target_list') - api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ .AndReturn(self.floating_ips.list()) - api.nova.server_list(IsA(http.HttpRequest)) \ + api.network.floating_ip_target_list(IsA(http.HttpRequest)) \ .AndReturn(self.servers.list()) - api.nova.server_add_floating_ip(IsA(http.HttpRequest), - server.id, - floating_ip.id) + api.network.floating_ip_associate(IsA(http.HttpRequest), + floating_ip.id, + server.id) self.mox.ReplayAll() form_data = {'instance_id': server.id, @@ -103,17 +103,17 @@ class FloatingIpViewTests(test.TestCase): def test_associate_post_with_exception(self): floating_ip = self.floating_ips.list()[1] server = self.servers.first() - self.mox.StubOutWithMock(api.nova, 'server_add_floating_ip') - self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_list') - self.mox.StubOutWithMock(api.nova, 'server_list') + self.mox.StubOutWithMock(api.network, 'floating_ip_associate') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') + self.mox.StubOutWithMock(api.network, 'floating_ip_target_list') - api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ .AndReturn(self.floating_ips.list()) - api.nova.server_list(IsA(http.HttpRequest)) \ + api.network.floating_ip_target_list(IsA(http.HttpRequest)) \ .AndReturn(self.servers.list()) - api.nova.server_add_floating_ip(IsA(http.HttpRequest), - server.id, - floating_ip.id) \ + api.network.floating_ip_associate(IsA(http.HttpRequest), + floating_ip.id, + server.id) \ .AndRaise(self.exceptions.nova) self.mox.ReplayAll() @@ -128,9 +128,9 @@ class FloatingIpViewTests(test.TestCase): server = self.servers.first() self.mox.StubOutWithMock(api.nova, 'keypair_list') self.mox.StubOutWithMock(api.nova, 'security_group_list') - self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_list') - self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_get') - self.mox.StubOutWithMock(api.nova, 'server_remove_floating_ip') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_get') + self.mox.StubOutWithMock(api.network, 'floating_ip_disassociate') self.mox.StubOutWithMock(api.nova, 'server_list') api.nova.server_list(IsA(http.HttpRequest), @@ -139,11 +139,11 @@ class FloatingIpViewTests(test.TestCase): .AndReturn(self.keypairs.list()) api.nova.security_group_list(IsA(http.HttpRequest)) \ .AndReturn(self.security_groups.list()) - api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ .AndReturn(self.floating_ips.list()) - api.nova.server_remove_floating_ip(IsA(http.HttpRequest), - server.id, - floating_ip.id) + api.network.floating_ip_disassociate(IsA(http.HttpRequest), + floating_ip.id, + server.id) self.mox.ReplayAll() action = "floating_ips__disassociate__%s" % floating_ip.id @@ -156,9 +156,9 @@ class FloatingIpViewTests(test.TestCase): server = self.servers.first() self.mox.StubOutWithMock(api.nova, 'keypair_list') self.mox.StubOutWithMock(api.nova, 'security_group_list') - self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_list') - self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_get') - self.mox.StubOutWithMock(api.nova, 'server_remove_floating_ip') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_get') + self.mox.StubOutWithMock(api.network, 'floating_ip_disassociate') self.mox.StubOutWithMock(api.nova, 'server_list') api.nova.server_list(IsA(http.HttpRequest), @@ -167,13 +167,13 @@ class FloatingIpViewTests(test.TestCase): .AndReturn(self.keypairs.list()) api.nova.security_group_list(IsA(http.HttpRequest)) \ .AndReturn(self.security_groups.list()) - api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ .AndReturn(self.floating_ips.list()) - api.nova.server_remove_floating_ip(IsA(http.HttpRequest), - server.id, - floating_ip.id) \ - .AndRaise(self.exceptions.nova) + api.network.floating_ip_disassociate(IsA(http.HttpRequest), + floating_ip.id, + server.id) \ + .AndRaise(self.exceptions.nova) self.mox.ReplayAll() action = "floating_ips__disassociate__%s" % floating_ip.id @@ -184,8 +184,13 @@ class FloatingIpViewTests(test.TestCase): class FloatingIpQuantumViewTests(FloatingIpViewTests): def setUp(self): super(FloatingIpViewTests, self).setUp() + self._floating_ips_orig = self.floating_ips self.floating_ips = self.floating_ips_uuid + def tearDown(self): + self.floating_ips = self._floating_ips_orig + super(FloatingIpViewTests, self).tearDown() + class FloatingIpUtilsTests(test.TestCase): def test_accept_valid_integer(self): diff --git a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/views.py b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/views.py index f90dcbdb6..d120b752b 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/views.py +++ b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/views.py @@ -59,12 +59,12 @@ class AllocateView(forms.ModalFormView): def get_initial(self): try: - pools = api.nova.floating_ip_pools_list(self.request) + pools = api.network.floating_ip_pools_list(self.request) except: pools = [] exceptions.handle(self.request, _("Unable to retrieve floating IP pools.")) - pool_list = [(pool.name, pool.name) for pool in pools] + pool_list = [(pool.id, pool.name) for pool in pools] if not pool_list: pool_list = [(None, _("No floating IP pools available."))] return {'pool_list': pool_list} diff --git a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/workflows.py b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/workflows.py index 48628308e..82b34072f 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/workflows.py +++ b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/workflows.py @@ -42,15 +42,34 @@ class AssociateIPAction(workflows.Action): help_text = _("Select the IP address you wish to associate with " "the selected instance.") + def __init__(self, *args, **kwargs): + super(AssociateIPAction, self).__init__(*args, **kwargs) + if api.base.is_service_enabled(self.request, 'network'): + label = _("Port to be associated") + else: + label = _("Instance to be associated") + self.fields['instance_id'].label = label + + # If AssociateIP is invoked from instance menu, instance_id parameter + # is passed in URL. In Quantum based Floating IP implementation + # an association target is not an instance but a port, so we need + # to get an association target based on a received instance_id + # and set the initial value of instance_id ChoiceField. + q_instance_id = self.request.GET.get('instance_id') + if q_instance_id: + target_id = api.network.floating_ip_target_get_by_instance( + self.request, q_instance_id) + self.initial['instance_id'] = target_id + def populate_ip_id_choices(self, request, context): try: - ips = api.nova.tenant_floating_ip_list(self.request) + ips = api.network.tenant_floating_ip_list(self.request) except: redirect = reverse('horizon:project:access_and_security:index') exceptions.handle(self.request, _('Unable to retrieve floating IP addresses.'), redirect=redirect) - options = sorted([(ip.id, ip.ip) for ip in ips if not ip.instance_id]) + options = sorted([(ip.id, ip.ip) for ip in ips if not ip.port_id]) if options: options.insert(0, ("", _("Select an IP address"))) else: @@ -60,24 +79,32 @@ class AssociateIPAction(workflows.Action): def populate_instance_id_choices(self, request, context): try: - servers = api.nova.server_list(self.request) + targets = api.network.floating_ip_target_list(self.request) except: redirect = reverse('horizon:project:access_and_security:index') exceptions.handle(self.request, _('Unable to retrieve instance list.'), redirect=redirect) instances = [] - for server in servers: - server_name = "%s (%s)" % (server.name, server.id) - instances.append((server.id, server_name)) + for target in targets: + instances.append((target.id, target.name)) # Sort instances for easy browsing instances = sorted(instances, key=lambda x: x[1]) + quantum_enabled = api.base.is_service_enabled(request, 'network') if instances: - instances.insert(0, ("", _("Select an instance"))) + if quantum_enabled: + label = _("Select a port") + else: + label = _("Select an instance") + instances.insert(0, ("", label)) else: - instances = (("", _("No instances available")),) + if quantum_enabled: + label = _("No ports available") + else: + label = _("No instances available") + instances = (("", label),) return instances @@ -108,9 +135,9 @@ class IPAssociationWorkflow(workflows.Workflow): def handle(self, request, data): try: - api.nova.server_add_floating_ip(request, - data['instance_id'], - data['ip_id']) + api.network.floating_ip_associate(request, + data['ip_id'], + data['instance_id']) except: exceptions.handle(request) return False diff --git a/openstack_dashboard/dashboards/project/access_and_security/keypairs/tests.py b/openstack_dashboard/dashboards/project/access_and_security/keypairs/tests.py index 650ad229a..42077df8e 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/keypairs/tests.py +++ b/openstack_dashboard/dashboards/project/access_and_security/keypairs/tests.py @@ -37,15 +37,15 @@ class KeyPairViewTests(test.TestCase): self.mox.StubOutWithMock(api.nova, 'keypair_list') self.mox.StubOutWithMock(api.nova, 'keypair_delete') self.mox.StubOutWithMock(api.nova, 'security_group_list') - self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_list') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') self.mox.StubOutWithMock(api.nova, 'server_list') api.nova.server_list(IsA(http.HttpRequest), all_tenants=True).AndReturn(self.servers.list()) api.nova.security_group_list(IsA(http.HttpRequest)) \ - .AndReturn(self.security_groups.list()) - api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ - .AndReturn(self.floating_ips.list()) + .AndReturn(self.security_groups.list()) + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) api.nova.keypair_list(IsA(http.HttpRequest)) \ .AndReturn(self.keypairs.list()) api.nova.keypair_delete(IsA(http.HttpRequest), keypair.name) @@ -60,15 +60,15 @@ class KeyPairViewTests(test.TestCase): self.mox.StubOutWithMock(api.nova, 'keypair_list') self.mox.StubOutWithMock(api.nova, 'keypair_delete') self.mox.StubOutWithMock(api.nova, 'security_group_list') - self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_list') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') self.mox.StubOutWithMock(api.nova, 'server_list') api.nova.server_list(IsA(http.HttpRequest), all_tenants=True).AndReturn(self.servers.list()) api.nova.security_group_list(IsA(http.HttpRequest)) \ - .AndReturn(self.security_groups.list()) - api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ - .AndReturn(self.floating_ips.list()) + .AndReturn(self.security_groups.list()) + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) api.nova.keypair_list(IsA(http.HttpRequest)) \ .AndReturn(self.keypairs.list()) api.nova.keypair_delete(IsA(http.HttpRequest), keypair.name) \ diff --git a/openstack_dashboard/dashboards/project/access_and_security/tests.py b/openstack_dashboard/dashboards/project/access_and_security/tests.py index cf8017707..6b65bf2f6 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/tests.py +++ b/openstack_dashboard/dashboards/project/access_and_security/tests.py @@ -30,11 +30,14 @@ from openstack_dashboard.test import helpers as test class AccessAndSecurityTests(test.TestCase): + def setUp(self): + super(AccessAndSecurityTests, self).setUp() + def test_index(self): keypairs = self.keypairs.list() sec_groups = self.security_groups.list() floating_ips = self.floating_ips.list() - self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_list') + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') self.mox.StubOutWithMock(api.nova, 'security_group_list') self.mox.StubOutWithMock(api.nova, 'keypair_list') self.mox.StubOutWithMock(api.nova, 'server_list') @@ -42,7 +45,7 @@ class AccessAndSecurityTests(test.TestCase): api.nova.server_list(IsA(http.HttpRequest), all_tenants=True).AndReturn(self.servers.list()) api.nova.keypair_list(IsA(http.HttpRequest)).AndReturn(keypairs) - api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ .AndReturn(floating_ips) api.nova.security_group_list(IsA(http.HttpRequest)) \ .AndReturn(sec_groups) @@ -60,21 +63,24 @@ class AccessAndSecurityTests(test.TestCase): floating_ips) def test_association(self): - servers = self.servers.list() - - # Add duplicate instance name to test instance name with [IP] - # change id and private IP + servers = [api.nova.Server(s, self.request) + for s in self.servers.list()] + # Add duplicate instance name to test instance name with [ID] + # Change id and private IP server3 = api.nova.Server(self.servers.first(), self.request) server3.id = 101 server3.addresses = deepcopy(server3.addresses) server3.addresses['private'][0]['addr'] = "10.0.0.5" - self.servers.add(server3) + servers.append(server3) - self.mox.StubOutWithMock(api.nova, 'tenant_floating_ip_list') - self.mox.StubOutWithMock(api.nova, 'server_list') - api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + targets = [api.nova.FloatingIpTarget(s) for s in servers] + + self.mox.StubOutWithMock(api.network, 'tenant_floating_ip_list') + self.mox.StubOutWithMock(api.network, 'floating_ip_target_list') + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ .AndReturn(self.floating_ips.list()) - api.nova.server_list(IsA(http.HttpRequest)).AndReturn(servers) + api.network.floating_ip_target_list(IsA(http.HttpRequest)) \ + .AndReturn(targets) self.mox.ReplayAll() res = self.client.get(reverse("horizon:project:access_and_security:" @@ -89,7 +95,7 @@ class AccessAndSecurityTests(test.TestCase): self.assertContains(res, '') -class AccessAndSecurityQuantumTests(AccessAndSecurityTests): +class AccessAndSecurityQuantumProxyTests(AccessAndSecurityTests): def setUp(self): - super(AccessAndSecurityTests, self).setUp() + super(AccessAndSecurityQuantumProxyTests, self).setUp() self.floating_ips = self.floating_ips_uuid diff --git a/openstack_dashboard/dashboards/project/access_and_security/views.py b/openstack_dashboard/dashboards/project/access_and_security/views.py index a8ac6fc21..15412265a 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/views.py +++ b/openstack_dashboard/dashboards/project/access_and_security/views.py @@ -27,9 +27,11 @@ import logging from django.utils.translation import ugettext_lazy as _ from horizon import exceptions +from horizon import messages from horizon import tables -from openstack_dashboard import api +from openstack_dashboard.api import network +from openstack_dashboard.api import nova from .keypairs.tables import KeypairsTable from .floating_ips.tables import FloatingIPsTable from .security_groups.tables import SecurityGroupsTable @@ -44,7 +46,7 @@ class IndexView(tables.MultiTableView): def get_keypairs_data(self): try: - keypairs = api.nova.keypair_list(self.request) + keypairs = nova.keypair_list(self.request) except: keypairs = [] exceptions.handle(self.request, @@ -53,7 +55,7 @@ class IndexView(tables.MultiTableView): def get_security_groups_data(self): try: - security_groups = api.nova.security_group_list(self.request) + security_groups = nova.security_group_list(self.request) except: security_groups = [] exceptions.handle(self.request, @@ -62,15 +64,23 @@ class IndexView(tables.MultiTableView): def get_floating_ips_data(self): try: - floating_ips = api.nova.tenant_floating_ip_list(self.request) + floating_ips = network.tenant_floating_ip_list(self.request) except: floating_ips = [] exceptions.handle(self.request, _('Unable to retrieve floating IP addresses.')) + try: + floating_ip_pools = network.floating_ip_pools_list(self.request) + except: + floating_ip_pools = [] + messages.warning(self.request, + _('Unable to retrieve floating IP pools.')) + pool_dict = dict([(obj.id, obj.name) for obj in floating_ip_pools]) + instances = [] try: - instances = api.nova.server_list(self.request, all_tenants=True) + instances = nova.server_list(self.request, all_tenants=True) except: exceptions.handle(self.request, _('Unable to retrieve instance list.')) @@ -80,5 +90,6 @@ class IndexView(tables.MultiTableView): for ip in floating_ips: ip.instance_name = instances_dict[ip.instance_id].name \ if ip.instance_id in instances_dict else None + ip.pool_name = pool_dict.get(ip.pool, ip.pool) return floating_ips diff --git a/openstack_dashboard/dashboards/project/instances/tables.py b/openstack_dashboard/dashboards/project/instances/tables.py index 2870d0252..df1bf309a 100644 --- a/openstack_dashboard/dashboards/project/instances/tables.py +++ b/openstack_dashboard/dashboards/project/instances/tables.py @@ -276,7 +276,8 @@ class AssociateIP(tables.LinkAction): classes = ("ajax-modal", "btn-associate") def allowed(self, request, instance): - if HORIZON_CONFIG["simple_ip_management"]: + fip = api.network.NetworkClient(request).floating_ips + if fip.is_simple_associate_supported(): return False return not is_deleting(instance) @@ -290,19 +291,20 @@ class AssociateIP(tables.LinkAction): class SimpleAssociateIP(tables.Action): - name = "associate" + name = "associate-simple" verbose_name = _("Associate Floating IP") - classes = ("btn-associate",) + classes = ("btn-associate-simple",) def allowed(self, request, instance): - if not HORIZON_CONFIG["simple_ip_management"]: + fip = api.network.NetworkClient(request).floating_ips + if not fip.is_simple_associate_supported(): return False return not is_deleting(instance) def single(self, table, request, instance): try: - fip = api.nova.tenant_floating_ip_allocate(request) - api.nova.server_add_floating_ip(request, instance, fip.id) + fip = api.network.tenant_floating_ip_allocate(request) + api.network.floating_ip_associate(request, fip.id, instance) messages.success(request, _("Successfully associated floating IP: %s") % fip.ip) @@ -312,12 +314,6 @@ class SimpleAssociateIP(tables.Action): return shortcuts.redirect("horizon:project:instances:index") -if HORIZON_CONFIG["simple_ip_management"]: - CurrentAssociateIP = SimpleAssociateIP -else: - CurrentAssociateIP = AssociateIP - - class SimpleDisassociateIP(tables.Action): name = "disassociate" verbose_name = _("Disassociate Floating IP") @@ -330,16 +326,15 @@ class SimpleDisassociateIP(tables.Action): def single(self, table, request, instance_id): try: - fips = [fip for fip in api.nova.tenant_floating_ip_list(request) - if fip.instance_id == instance_id] + fips = [fip for fip in api.network.tenant_floating_ip_list(request) + if fip.port_id == instance_id] # Removing multiple floating IPs at once doesn't work, so this pops # off the first one. if fips: fip = fips.pop() - api.nova.server_remove_floating_ip(request, - instance_id, - fip.id) - api.nova.tenant_floating_ip_release(request, fip.id) + api.network.floating_ip_disassociate(request, + fip.id, instance_id) + api.network.tenant_floating_ip_release(request, fip.id) messages.success(request, _("Successfully disassociated " "floating IP: %s") % fip.ip) @@ -452,6 +447,7 @@ class InstancesTable(tables.DataTable): row_class = UpdateRow table_actions = (LaunchLink, TerminateInstance) row_actions = (ConfirmResize, RevertResize, CreateSnapshot, - CurrentAssociateIP, SimpleDisassociateIP, EditInstance, + SimpleAssociateIP, AssociateIP, + SimpleDisassociateIP, EditInstance, ConsoleLink, LogLink, TogglePause, ToggleSuspend, RebootInstance, TerminateInstance) diff --git a/openstack_dashboard/test/api_tests/network_tests.py b/openstack_dashboard/test/api_tests/network_tests.py new file mode 100644 index 000000000..4e8d187ab --- /dev/null +++ b/openstack_dashboard/test/api_tests/network_tests.py @@ -0,0 +1,291 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 NEC Corporation +# +# 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 django import http +from mox import IsA + +from novaclient.v1_1.floating_ip_pools import FloatingIPPool + +from openstack_dashboard import api +from openstack_dashboard.test import helpers as test + + +class NetworkApiNovaFloatingIpTests(test.APITestCase): + def setUp(self): + super(NetworkApiNovaFloatingIpTests, self).setUp() + self.mox.StubOutWithMock(api.base, 'is_service_enabled') + api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \ + .AndReturn(False) + + def test_floating_ip_pools_list(self): + pool_names = ['pool1', 'pool2'] + pools = [FloatingIPPool(None, {'name': pool}) for pool in pool_names] + novaclient = self.stub_novaclient() + novaclient.floating_ip_pools = self.mox.CreateMockAnything() + novaclient.floating_ip_pools.list().AndReturn(pools) + self.mox.ReplayAll() + + ret = api.network.floating_ip_pools_list(self.request) + self.assertEqual([p.name for p in ret], pool_names) + + def test_floating_ip_list(self): + fips = self.api_floating_ips.list() + novaclient = self.stub_novaclient() + novaclient.floating_ips = self.mox.CreateMockAnything() + novaclient.floating_ips.list().AndReturn(fips) + self.mox.ReplayAll() + + ret = api.network.tenant_floating_ip_list(self.request) + for r, e in zip(ret, fips): + for attr in ['id', 'ip', 'pool', 'fixed_ip', 'instance_id']: + self.assertEqual(r.__getattr__(attr), e.__getattr__(attr)) + self.assertEqual(r.port_id, e.instance_id) + + def test_floating_ip_get(self): + fip = self.api_floating_ips.first() + novaclient = self.stub_novaclient() + novaclient.floating_ips = self.mox.CreateMockAnything() + novaclient.floating_ips.get(fip.id).AndReturn(fip) + self.mox.ReplayAll() + + ret = api.network.tenant_floating_ip_get(self.request, fip.id) + for attr in ['id', 'ip', 'pool', 'fixed_ip', 'instance_id']: + self.assertEqual(ret.__getattr__(attr), fip.__getattr__(attr)) + self.assertEqual(ret.port_id, fip.instance_id) + + def test_floating_ip_allocate(self): + pool_name = 'fip_pool' + fip = self.api_floating_ips.first() + novaclient = self.stub_novaclient() + novaclient.floating_ips = self.mox.CreateMockAnything() + novaclient.floating_ips.create(pool=pool_name).AndReturn(fip) + self.mox.ReplayAll() + + ret = api.network.tenant_floating_ip_allocate(self.request, pool_name) + for attr in ['id', 'ip', 'pool', 'fixed_ip', 'instance_id']: + self.assertEqual(ret.__getattr__(attr), fip.__getattr__(attr)) + self.assertEqual(ret.port_id, fip.instance_id) + + def test_floating_ip_release(self): + fip = self.api_floating_ips.first() + novaclient = self.stub_novaclient() + novaclient.floating_ips = self.mox.CreateMockAnything() + novaclient.floating_ips.delete(fip.id) + self.mox.ReplayAll() + + api.network.tenant_floating_ip_release(self.request, fip.id) + + def test_floating_ip_associate(self): + server = api.nova.Server(self.servers.first(), self.request) + floating_ip = self.floating_ips.first() + + novaclient = self.stub_novaclient() + novaclient.floating_ips = self.mox.CreateMockAnything() + novaclient.servers = self.mox.CreateMockAnything() + novaclient.servers.get(server.id).AndReturn(server) + novaclient.floating_ips.get(floating_ip.id).AndReturn(floating_ip) + novaclient.servers.add_floating_ip(server.id, floating_ip.ip) \ + .AndReturn(server) + self.mox.ReplayAll() + + api.network.floating_ip_associate(self.request, + floating_ip.id, + server.id) + + def test_floating_ip_disassociate(self): + server = api.nova.Server(self.servers.first(), self.request) + floating_ip = self.api_floating_ips.first() + + novaclient = self.stub_novaclient() + novaclient.servers = self.mox.CreateMockAnything() + novaclient.floating_ips = self.mox.CreateMockAnything() + novaclient.servers.get(server.id).AndReturn(server) + novaclient.floating_ips.get(floating_ip.id).AndReturn(floating_ip) + novaclient.servers.remove_floating_ip(server.id, floating_ip.ip) \ + .AndReturn(server) + self.mox.ReplayAll() + + api.network.floating_ip_disassociate(self.request, + floating_ip.id, + server.id) + + def test_floating_ip_target_list(self): + servers = self.servers.list() + novaclient = self.stub_novaclient() + novaclient.servers = self.mox.CreateMockAnything() + novaclient.servers.list().AndReturn(servers) + self.mox.ReplayAll() + + targets = api.network.floating_ip_target_list(self.request) + for target, server in zip(targets, servers): + self.assertEqual(target.id, server.id) + self.assertEqual(target.name, '%s (%s)' % (server.name, server.id)) + + def test_floating_ip_target_get_by_instance(self): + self.mox.ReplayAll() + instance_id = self.servers.first().id + ret = api.network.floating_ip_target_get_by_instance(self.request, + instance_id) + self.assertEqual(instance_id, ret) + + +class NetworkApiQuantumFloatingIpTests(test.APITestCase): + def setUp(self): + super(NetworkApiQuantumFloatingIpTests, self).setUp() + self.mox.StubOutWithMock(api.base, 'is_service_enabled') + api.base.is_service_enabled(IsA(http.HttpRequest), 'network') \ + .AndReturn(True) + self.qclient = self.stub_quantumclient() + + def test_floating_ip_pools_list(self): + search_opts = {'router:external': True} + ext_nets = [n for n in self.api_networks.list() + if n['router:external']] + self.qclient.list_networks(**search_opts) \ + .AndReturn({'networks': ext_nets}) + self.mox.ReplayAll() + + rets = api.network.floating_ip_pools_list(self.request) + for attr in ['id', 'name']: + self.assertEqual([p.__getattr__(attr) for p in rets], + [p[attr] for p in ext_nets]) + + def test_floating_ip_list(self): + fips = self.api_q_floating_ips.list() + self.qclient.list_floatingips().AndReturn({'floatingips': fips}) + self.qclient.list_ports().AndReturn({'ports': self.api_ports.list()}) + self.mox.ReplayAll() + + rets = api.network.tenant_floating_ip_list(self.request) + assoc_port = self.api_ports.list()[1] + self.assertEqual(len(fips), len(rets)) + for ret, exp in zip(rets, fips): + for attr in ['id', 'ip', 'pool', 'fixed_ip', 'port_id']: + self.assertEqual(ret.__getattr__(attr), exp[attr]) + if exp['port_id']: + dev_id = assoc_port['device_id'] if exp['port_id'] else None + self.assertEqual(ret.instance_id, dev_id) + + def test_floating_ip_get_associated(self): + fip = self.api_q_floating_ips.list()[1] + assoc_port = self.api_ports.list()[1] + self.qclient.show_floatingip(fip['id']).AndReturn({'floatingip': fip}) + self.qclient.show_port(assoc_port['id']) \ + .AndReturn({'port': assoc_port}) + self.mox.ReplayAll() + + ret = api.network.tenant_floating_ip_get(self.request, fip['id']) + for attr in ['id', 'ip', 'pool', 'fixed_ip', 'port_id']: + self.assertEqual(ret.__getattr__(attr), fip[attr]) + self.assertEqual(ret.instance_id, assoc_port['device_id']) + + def test_floating_ip_get_unassociated(self): + fip = self.api_q_floating_ips.list()[0] + self.qclient.show_floatingip(fip['id']).AndReturn({'floatingip': fip}) + self.mox.ReplayAll() + + ret = api.network.tenant_floating_ip_get(self.request, fip['id']) + for attr in ['id', 'ip', 'pool', 'fixed_ip', 'port_id']: + self.assertEqual(ret.__getattr__(attr), fip[attr]) + self.assertEqual(ret.instance_id, None) + + def test_floating_ip_allocate(self): + ext_nets = [n for n in self.api_networks.list() + if n['router:external']] + ext_net = ext_nets[0] + fip = self.api_q_floating_ips.first() + self.qclient.create_floatingip( + {'floatingip': {'floating_network_id': ext_net['id']}}) \ + .AndReturn({'floatingip': fip}) + self.mox.ReplayAll() + + ret = api.network.tenant_floating_ip_allocate(self.request, + ext_net['id']) + for attr in ['id', 'ip', 'pool', 'fixed_ip', 'port_id']: + self.assertEqual(ret.__getattr__(attr), fip[attr]) + self.assertEqual(ret.instance_id, None) + + def test_floating_ip_release(self): + fip = self.api_q_floating_ips.first() + self.qclient.delete_floatingip(fip['id']) + self.mox.ReplayAll() + + api.network.tenant_floating_ip_release(self.request, fip['id']) + + def test_floating_ip_associate(self): + fip = self.api_q_floating_ips.list()[1] + assoc_port = self.api_ports.list()[1] + ip_address = assoc_port['fixed_ips'][0]['ip_address'] + target_id = '%s_%s' % (assoc_port['id'], ip_address) + params = {'port_id': assoc_port['id'], + 'fixed_ip_address': ip_address} + self.qclient.update_floatingip(fip['id'], + {'floatingip': params}) + self.mox.ReplayAll() + + api.network.floating_ip_associate(self.request, fip['id'], target_id) + + def test_floating_ip_disassociate(self): + fip = self.api_q_floating_ips.list()[1] + assoc_port = self.api_ports.list()[1] + ip_address = assoc_port['fixed_ips'][0]['ip_address'] + target_id = '%s_%s' % (assoc_port['id'], ip_address) + self.qclient.update_floatingip(fip['id'], + {'floatingip': {'port_id': None}}) + self.mox.ReplayAll() + + api.network.floating_ip_disassociate(self.request, fip['id'], + target_id) + + def _get_target_id(self, port): + param = {'id': port['id'], + 'addr': port['fixed_ips'][0]['ip_address']} + return '%(id)s_%(addr)s' % param + + def _get_target_name(self, port): + param = {'svrid': port['device_id'], + 'addr': port['fixed_ips'][0]['ip_address']} + return 'server_%(svrid)s: %(addr)s' % param + + def test_floating_ip_target_list(self): + ports = self.api_ports.list() + target_ports = [(self._get_target_id(p), + self._get_target_name(p)) for p in ports + if not p['device_owner'].startswith('network:')] + + self.qclient.list_ports().AndReturn({'ports': ports}) + servers = self.servers.list() + novaclient = self.stub_novaclient() + novaclient.servers = self.mox.CreateMockAnything() + search_opts = {'project_id': self.request.user.tenant_id} + novaclient.servers.list(True, search_opts).AndReturn(servers) + self.mox.ReplayAll() + + rets = api.network.floating_ip_target_list(self.request) + self.assertEqual(len(rets), len(target_ports)) + for ret, exp in zip(rets, target_ports): + self.assertEqual(ret.id, exp[0]) + self.assertEqual(ret.name, exp[1]) + + def test_floating_ip_target_get_by_instance(self): + ports = self.api_ports.list() + candidates = [p for p in ports if p['device_id'] == '1'] + search_opts = {'device_id': '1'} + self.qclient.list_ports(**search_opts).AndReturn({'ports': candidates}) + self.mox.ReplayAll() + + ret = api.network.floating_ip_target_get_by_instance(self.request, '1') + self.assertEqual(ret, self._get_target_id(candidates[0])) diff --git a/openstack_dashboard/test/api_tests/nova_tests.py b/openstack_dashboard/test/api_tests/nova_tests.py index 9e4c96345..6e9d97cd3 100644 --- a/openstack_dashboard/test/api_tests/nova_tests.py +++ b/openstack_dashboard/test/api_tests/nova_tests.py @@ -138,42 +138,6 @@ class ComputeApiTests(test.APITestCase): ret_val = api.nova.server_get(self.request, server.id) self.assertIsInstance(ret_val, api.nova.Server) - def test_server_remove_floating_ip(self): - server = api.nova.Server(self.servers.first(), self.request) - floating_ip = self.floating_ips.first() - - novaclient = self.stub_novaclient() - novaclient.servers = self.mox.CreateMockAnything() - novaclient.floating_ips = self.mox.CreateMockAnything() - novaclient.servers.get(server.id).AndReturn(server) - novaclient.floating_ips.get(floating_ip.id).AndReturn(floating_ip) - novaclient.servers.remove_floating_ip(server.id, floating_ip.ip) \ - .AndReturn(server) - self.mox.ReplayAll() - - server = api.nova.server_remove_floating_ip(self.request, - server.id, - floating_ip.id) - self.assertIsInstance(server, api.nova.Server) - - def test_server_add_floating_ip(self): - server = api.nova.Server(self.servers.first(), self.request) - floating_ip = self.floating_ips.first() - novaclient = self.stub_novaclient() - - novaclient.floating_ips = self.mox.CreateMockAnything() - novaclient.servers = self.mox.CreateMockAnything() - novaclient.servers.get(server.id).AndReturn(server) - novaclient.floating_ips.get(floating_ip.id).AndReturn(floating_ip) - novaclient.servers.add_floating_ip(server.id, floating_ip.ip) \ - .AndReturn(server) - self.mox.ReplayAll() - - server = api.nova.server_add_floating_ip(self.request, - server.id, - floating_ip.id) - self.assertIsInstance(server, api.nova.Server) - def test_absolute_limits_handle_unlimited(self): values = {"maxTotalCores": -1, "maxTotalInstances": 10} limits = self.mox.CreateMockAnything() diff --git a/openstack_dashboard/test/test_data/nova_data.py b/openstack_dashboard/test/test_data/nova_data.py index 89b2ef2a0..429d84a10 100644 --- a/openstack_dashboard/test/test_data/nova_data.py +++ b/openstack_dashboard/test/test_data/nova_data.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import json import uuid @@ -23,6 +24,7 @@ from novaclient.v1_1 import (flavors, keypairs, servers, volumes, security_groups as sec_groups) from openstack_dashboard.api.base import Quota, QuotaSet as QuotaSetWrapper +from openstack_dashboard.api.nova import FloatingIp as NetFloatingIp from openstack_dashboard.usage.quotas import QuotaUsage from .utils import TestDataContainer @@ -150,6 +152,11 @@ def data(TEST): TEST.volume_snapshots = TestDataContainer() TEST.volume_types = TestDataContainer() + # Data return by novaclient. + # It is used if API layer does data conversion. + TEST.api_floating_ips = TestDataContainer() + TEST.api_floating_ips_uuid = TestDataContainer() + # Volumes volume = volumes.Volume(volumes.VolumeManager(None), dict(id="41023e92-8008-4c8b-8059-7f2293ff3775", @@ -356,26 +363,36 @@ def data(TEST): {'id': 1, 'fixed_ip': '10.0.0.4', 'instance_id': server_1.id, - 'ip': '58.58.58.58'}) + 'ip': '58.58.58.58', + 'pool': 'pool1'}) fip_2 = floating_ips.FloatingIP(floating_ips.FloatingIPManager(None), {'id': 2, 'fixed_ip': None, 'instance_id': None, - 'ip': '58.58.58.58'}) - TEST.floating_ips.add(fip_1, fip_2) + 'ip': '58.58.58.58', + 'pool': 'pool2'}) + TEST.api_floating_ips.add(fip_1, fip_2) - # Floating IP with UUID id (for Floating IP with Quantum) + TEST.floating_ips.add(NetFloatingIp(copy.deepcopy(fip_1)), + NetFloatingIp(copy.deepcopy(fip_2))) + + # Floating IP with UUID id (for Floating IP with Quantum Proxy) fip_3 = floating_ips.FloatingIP(floating_ips.FloatingIPManager(None), {'id': str(uuid.uuid4()), 'fixed_ip': '10.0.0.4', 'instance_id': server_1.id, - 'ip': '58.58.58.58'}) + 'ip': '58.58.58.58', + 'pool': 'pool1'}) fip_4 = floating_ips.FloatingIP(floating_ips.FloatingIPManager(None), {'id': str(uuid.uuid4()), 'fixed_ip': None, 'instance_id': None, - 'ip': '58.58.58.58'}) - TEST.floating_ips_uuid.add(fip_3, fip_4) + 'ip': '58.58.58.58', + 'pool': 'pool2'}) + TEST.api_floating_ips_uuid.add(fip_3, fip_4) + + TEST.floating_ips_uuid.add(NetFloatingIp(copy.deepcopy(fip_3)), + NetFloatingIp(copy.deepcopy(fip_4))) # Usage usage_vals = {"tenant_id": TEST.tenant.id, diff --git a/openstack_dashboard/test/test_data/quantum_data.py b/openstack_dashboard/test/test_data/quantum_data.py index 7c60323fa..f004bdc1e 100644 --- a/openstack_dashboard/test/test_data/quantum_data.py +++ b/openstack_dashboard/test/test_data/quantum_data.py @@ -14,7 +14,8 @@ import copy -from openstack_dashboard.api.quantum import Network, Subnet, Port, Router +from openstack_dashboard.api.quantum import (Network, Subnet, Port, + Router, FloatingIp) from .utils import TestDataContainer @@ -25,13 +26,16 @@ def data(TEST): TEST.subnets = TestDataContainer() TEST.ports = TestDataContainer() TEST.routers = TestDataContainer() + TEST.q_floating_ips = TestDataContainer() # data return by quantumclient TEST.api_networks = TestDataContainer() TEST.api_subnets = TestDataContainer() TEST.api_ports = TestDataContainer() TEST.api_routers = TestDataContainer() + TEST.api_q_floating_ips = TestDataContainer() + #------------------------------------------------------------ # 1st network network_dict = {'admin_state_up': True, 'id': '82288d84-e0a5-42ac-95be-e6af08727e42', @@ -53,6 +57,17 @@ def data(TEST): 'name': 'mysubnet1', 'network_id': network_dict['id'], 'tenant_id': network_dict['tenant_id']} + + TEST.api_networks.add(network_dict) + TEST.api_subnets.add(subnet_dict) + + network = copy.deepcopy(network_dict) + subnet = Subnet(subnet_dict) + network['subnets'] = [subnet] + TEST.networks.add(Network(network)) + TEST.subnets.add(subnet) + + # ports on 1st network port_dict = {'admin_state_up': True, 'device_id': 'af75c8e5-a1cc-4567-8d04-44fcd6922890', 'device_owner': 'network:dhcp', @@ -64,17 +79,25 @@ def data(TEST): 'network_id': network_dict['id'], 'status': 'ACTIVE', 'tenant_id': network_dict['tenant_id']} - TEST.api_networks.add(network_dict) - TEST.api_subnets.add(subnet_dict) TEST.api_ports.add(port_dict) - - network = copy.deepcopy(network_dict) - subnet = Subnet(subnet_dict) - network['subnets'] = [subnet] - TEST.networks.add(Network(network)) - TEST.subnets.add(subnet) TEST.ports.add(Port(port_dict)) + port_dict = {'admin_state_up': True, + 'device_id': '1', + 'device_owner': 'compute:nova', + 'fixed_ips': [{'ip_address': '10.0.0.4', + 'subnet_id': subnet_dict['id']}], + 'id': '7e6ce62c-7ea2-44f8-b6b4-769af90a8406', + 'mac_address': 'fa:16:3e:9d:e6:2f', + 'name': '', + 'network_id': network_dict['id'], + 'status': 'ACTIVE', + 'tenant_id': network_dict['tenant_id']} + TEST.api_ports.add(port_dict) + TEST.ports.add(Port(port_dict)) + assoc_port = port_dict + + #------------------------------------------------------------ # 2nd network network_dict = {'admin_state_up': True, 'id': '72c3ab6c-c80f-4341-9dc5-210fa31ac6c2', @@ -82,7 +105,7 @@ def data(TEST): 'status': 'ACTIVE', 'subnets': ['3f7c5d79-ee55-47b0-9213-8e669fb03009'], 'tenant_id': '2', - 'router:external': True, + 'router:external': False, 'shared': True} subnet_dict = {'allocation_pools': [{'end': '172.16.88.254', 'start': '172.16.88.2'}], @@ -99,28 +122,66 @@ def data(TEST): 'name': 'aaaa', 'network_id': network_dict['id'], 'tenant_id': network_dict['tenant_id']} - port_dict = {'admin_state_up': True, - 'device_id': '40e536b1-b9fd-4eb7-82d6-84db5d65a2ac', - 'device_owner': 'compute:nova', - 'fixed_ips': [{'ip_address': '172.16.88.3', - 'subnet_id': subnet_dict['id']}], - 'id': '7e6ce62c-7ea2-44f8-b6b4-769af90a8406', - 'mac_address': 'fa:16:3e:56:e6:2f', - 'name': '', - 'network_id': network_dict['id'], - 'status': 'ACTIVE', - 'tenant_id': network_dict['tenant_id']} + TEST.api_networks.add(network_dict) TEST.api_subnets.add(subnet_dict) - TEST.api_ports.add(port_dict) network = copy.deepcopy(network_dict) subnet = Subnet(subnet_dict) network['subnets'] = [subnet] TEST.networks.add(Network(network)) TEST.subnets.add(subnet) + + port_dict = {'admin_state_up': True, + 'device_id': '2', + 'device_owner': 'compute:nova', + 'fixed_ips': [{'ip_address': '172.16.88.3', + 'subnet_id': subnet_dict['id']}], + 'id': '1db2cc37-3553-43fa-b7e2-3fc4eb4f9905', + 'mac_address': 'fa:16:3e:56:e6:2f', + 'name': '', + 'network_id': network_dict['id'], + 'status': 'ACTIVE', + 'tenant_id': network_dict['tenant_id']} + + TEST.api_ports.add(port_dict) TEST.ports.add(Port(port_dict)) + #------------------------------------------------------------ + # external network + network_dict = {'admin_state_up': True, + 'id': '9b466b94-213a-4cda-badf-72c102a874da', + 'name': 'ext_net', + 'status': 'ACTIVE', + 'subnets': ['d6bdc71c-7566-4d32-b3ff-36441ce746e8'], + 'tenant_id': '3', + 'router:external': True, + 'shared': False} + subnet_dict = {'allocation_pools': [{'start': '172.24.4.226.', + 'end': '172.24.4.238'}], + 'dns_nameservers': [], + 'host_routes': [], + 'cidr': '172.24.4.0/28', + 'enable_dhcp': False, + 'gateway_ip': '172.24.4.225', + 'id': 'd6bdc71c-7566-4d32-b3ff-36441ce746e8', + 'ip_version': 4, + 'name': 'ext_subnet', + 'network_id': network_dict['id'], + 'tenant_id': network_dict['tenant_id']} + ext_net = network_dict + ext_subnet = subnet_dict + + TEST.api_networks.add(network_dict) + TEST.api_subnets.add(subnet_dict) + + network = copy.deepcopy(network_dict) + subnet = Subnet(subnet_dict) + network['subnets'] = [subnet] + TEST.networks.add(Network(network)) + TEST.subnets.add(subnet) + + #------------------------------------------------------------ # Set up router data port_dict = {'admin_state_up': True, 'device_id': '7180cede-bcd8-4334-b19f-f7ef2f331f53', @@ -146,3 +207,27 @@ def data(TEST): 'tenant_id': '1'} TEST.api_routers.add(router_dict) TEST.routers.add(Router(router_dict)) + + #------------------------------------------------------------ + # floating IP + # unassociated + fip_dict = {'tenant_id': '1', + 'floating_ip_address': '172.16.88.227', + 'floating_network_id': ext_net['id'], + 'id': '9012cd70-cfae-4e46-b71e-6a409e9e0063', + 'fixed_ip_address': None, + 'port_id': None, + 'router_id': None} + TEST.api_q_floating_ips.add(fip_dict) + TEST.q_floating_ips.add(FloatingIp(fip_dict)) + + # associated (with compute port on 1st network) + fip_dict = {'tenant_id': '1', + 'floating_ip_address': '172.16.88.228', + 'floating_network_id': ext_net['id'], + 'id': 'a97af8f2-3149-4b97-abbd-e49ad19510f7', + 'fixed_ip_address': assoc_port['fixed_ips'][0]['ip_address'], + 'port_id': assoc_port['id'], + 'router_id': router_dict['id']} + TEST.api_q_floating_ips.add(fip_dict) + TEST.q_floating_ips.add(FloatingIp(fip_dict)) diff --git a/openstack_dashboard/test/tests/quotas.py b/openstack_dashboard/test/tests/quotas.py index 54a647f0b..9078db8f0 100644 --- a/openstack_dashboard/test/tests/quotas.py +++ b/openstack_dashboard/test/tests/quotas.py @@ -33,8 +33,8 @@ from openstack_dashboard.usage import quotas class QuotaTests(test.APITestCase): @test.create_stubs({api.nova: ('server_list', 'flavor_list', - 'tenant_floating_ip_list', 'tenant_quota_get',), + api.network: ('tenant_floating_ip_list',), quotas: ('is_service_enabled',), cinder: ('volume_list', 'tenant_quota_get',)}) def test_tenant_quota_usages(self): @@ -44,7 +44,7 @@ class QuotaTests(test.APITestCase): .AndReturn(self.flavors.list()) api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ .AndReturn(self.quotas.first()) - api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ .AndReturn(self.floating_ips.list()) api.nova.server_list(IsA(http.HttpRequest)) \ .AndReturn(self.servers.list()) @@ -73,8 +73,8 @@ class QuotaTests(test.APITestCase): @test.create_stubs({api.nova: ('server_list', 'flavor_list', - 'tenant_floating_ip_list', 'tenant_quota_get',), + api.network: ('tenant_floating_ip_list',), quotas: ('is_service_enabled',)}) def test_tenant_quota_usages_without_volume(self): quotas.is_service_enabled(IsA(http.HttpRequest), @@ -83,7 +83,7 @@ class QuotaTests(test.APITestCase): .AndReturn(self.flavors.list()) api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ .AndReturn(self.quotas.first()) - api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ .AndReturn(self.floating_ips.list()) api.nova.server_list(IsA(http.HttpRequest)) \ .AndReturn(self.servers.list()) @@ -106,8 +106,8 @@ class QuotaTests(test.APITestCase): @test.create_stubs({api.nova: ('server_list', 'flavor_list', - 'tenant_floating_ip_list', 'tenant_quota_get',), + api.network: ('tenant_floating_ip_list',), quotas: ('is_service_enabled',)}) def test_tenant_quota_usages_no_instances_running(self): quotas.is_service_enabled(IsA(http.HttpRequest), @@ -116,7 +116,8 @@ class QuotaTests(test.APITestCase): .AndReturn(self.flavors.list()) api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ .AndReturn(self.quotas.first()) - api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)).AndReturn([]) + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn([]) api.nova.server_list(IsA(http.HttpRequest)).AndReturn([]) self.mox.ReplayAll() @@ -137,8 +138,8 @@ class QuotaTests(test.APITestCase): @test.create_stubs({api.nova: ('server_list', 'flavor_list', - 'tenant_floating_ip_list', 'tenant_quota_get',), + api.network: ('tenant_floating_ip_list',), quotas: ('is_service_enabled',), cinder: ('volume_list', 'tenant_quota_get',)}) def test_tenant_quota_usages_unlimited_quota(self): @@ -151,7 +152,7 @@ class QuotaTests(test.APITestCase): .AndReturn(self.flavors.list()) api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ .AndReturn(inf_quota) - api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ .AndReturn(self.floating_ips.list()) api.nova.server_list(IsA(http.HttpRequest)) \ .AndReturn(self.servers.list()) diff --git a/openstack_dashboard/usage/quotas.py b/openstack_dashboard/usage/quotas.py index 3680d2fd0..363dddf63 100644 --- a/openstack_dashboard/usage/quotas.py +++ b/openstack_dashboard/usage/quotas.py @@ -4,7 +4,7 @@ import itertools from horizon import exceptions from horizon.utils.memoized import memoized -from openstack_dashboard.api import nova, cinder +from openstack_dashboard.api import nova, cinder, network from openstack_dashboard.api.base import is_service_enabled, QuotaSet @@ -85,7 +85,7 @@ def tenant_quota_usages(request): usages.add_quota(quota) # Get our usages. - floating_ips = nova.tenant_floating_ip_list(request) + floating_ips = network.tenant_floating_ip_list(request) flavors = dict([(f.id, f) for f in nova.flavor_list(request)]) instances = nova.server_list(request) # Fetch deleted flavors if necessary.