From 460a53f96dcaa3a66f059e902e3a20bdf3ba42ca Mon Sep 17 00:00:00 2001 From: Eddie Ramirez Date: Wed, 22 Jun 2016 16:55:05 +0000 Subject: [PATCH] Server-side filtering for Instances (Project/Admin) New Filters: - UUID - Flavor Name - Image Name - Availability Zone - Key Name - IPv4 Address - IPv6 - vCPUs - Changes-since. Affected views: Project->Instances and Admin->Instances Implements blueprint: server-side-filtering Change-Id: I3b07674fc1083607cae5d1db5a691827bde46d7c --- .../dashboards/admin/instances/tables.py | 13 ++- .../dashboards/admin/instances/tests.py | 86 ++++++++++++------- .../dashboards/admin/instances/views.py | 46 ++++++---- .../dashboards/project/instances/tables.py | 22 ++++- .../dashboards/project/instances/tests.py | 12 ++- .../dashboards/project/instances/views.py | 53 ++++++++---- 6 files changed, 156 insertions(+), 76 deletions(-) diff --git a/openstack_dashboard/dashboards/admin/instances/tables.py b/openstack_dashboard/dashboards/admin/instances/tables.py index b2f792afb6..bfb7694b3b 100644 --- a/openstack_dashboard/dashboards/admin/instances/tables.py +++ b/openstack_dashboard/dashboards/admin/instances/tables.py @@ -101,14 +101,11 @@ class AdminInstanceFilterAction(tables.FilterAction): # session property used for persisting the filter. name = "filter_admin_instances" filter_type = "server" - filter_choices = (('project', _("Project ="), True), - ('host', _("Host ="), True), - ('name', _("Name ="), True), - ('ip', _("IPv4 Address ="), True), - ('ip6', _("IPv6 Address ="), True), - ('status', _("Status ="), True), - ('image', _("Image ID ="), True), - ('flavor', _("Flavor ID ="), True)) + filter_choices = ( + ('project', _("Project Name ="), True), + ('tenant_id', _("Project ID ="), True), + ('host', _("Host Name ="), True), + ) + project_tables.INSTANCE_FILTER_CHOICES class AdminInstancesTable(tables.DataTable): diff --git a/openstack_dashboard/dashboards/admin/instances/tests.py b/openstack_dashboard/dashboards/admin/instances/tests.py index 5f6de0be12..79db385377 100644 --- a/openstack_dashboard/dashboards/admin/instances/tests.py +++ b/openstack_dashboard/dashboards/admin/instances/tests.py @@ -29,14 +29,17 @@ INDEX_URL = reverse('horizon:admin:instances:index') class InstanceViewTest(test.BaseAdminViewTests): - @test.create_stubs({api.nova: ('flavor_list', 'server_list', - 'extension_supported',), - api.keystone: ('tenant_list',), - api.network: ('servers_update_addresses',)}) + @test.create_stubs({ + api.nova: ('flavor_list', 'server_list', 'extension_supported',), + api.keystone: ('tenant_list',), + api.network: ('servers_update_addresses',), + api.glance: ('image_list_detailed',), + }) def test_index(self): servers = self.servers.list() flavors = self.flavors.list() tenants = self.tenants.list() + images = self.images.list() api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) api.nova.extension_supported('Shelve', IsA(http.HttpRequest)) \ @@ -44,12 +47,14 @@ class InstanceViewTest(test.BaseAdminViewTests): api.keystone.tenant_list(IsA(http.HttpRequest)).\ AndReturn([tenants, False]) search_opts = {'marker': None, 'paginate': True} + api.glance.image_list_detailed(IsA(http.HttpRequest))\ + .AndReturn(images) + api.nova.flavor_list(IsA(http.HttpRequest)).AndReturn(flavors) api.nova.server_list(IsA(http.HttpRequest), all_tenants=True, search_opts=search_opts) \ .AndReturn([servers, False]) api.network.servers_update_addresses(IsA(http.HttpRequest), servers, all_tenants=True) - api.nova.flavor_list(IsA(http.HttpRequest)).AndReturn(flavors) self.mox.ReplayAll() res = self.client.get(INDEX_URL) @@ -57,16 +62,18 @@ class InstanceViewTest(test.BaseAdminViewTests): instances = res.context['table'].data self.assertItemsEqual(instances, servers) - @test.create_stubs({api.nova: ('flavor_list', 'flavor_get', - 'server_list', 'extension_supported',), - api.keystone: ('tenant_list',), - api.network: ('servers_update_addresses',)}) + @test.create_stubs({ + api.nova: ('flavor_list', 'flavor_get', 'server_list', + 'extension_supported',), + api.keystone: ('tenant_list',), + api.network: ('servers_update_addresses',), + api.glance: ('image_list_detailed',), + }) def test_index_flavor_list_exception(self): servers = self.servers.list() tenants = self.tenants.list() flavors = self.flavors.list() full_flavors = OrderedDict([(f.id, f) for f in flavors]) - search_opts = {'marker': None, 'paginate': True} api.nova.server_list(IsA(http.HttpRequest), all_tenants=True, search_opts=search_opts) \ @@ -92,19 +99,26 @@ class InstanceViewTest(test.BaseAdminViewTests): instances = res.context['table'].data self.assertItemsEqual(instances, servers) - @test.create_stubs({api.nova: ('flavor_list', 'flavor_get', - 'server_list', 'extension_supported', ), - api.keystone: ('tenant_list',), - api.network: ('servers_update_addresses',)}) + @test.create_stubs({ + api.nova: ('flavor_list', 'flavor_get', 'server_list', + 'extension_supported',), + api.keystone: ('tenant_list',), + api.network: ('servers_update_addresses',), + api.glance: ('image_list_detailed',), + }) def test_index_flavor_get_exception(self): servers = self.servers.list() flavors = self.flavors.list() + images = self.images.list() tenants = self.tenants.list() # UUIDs generated using indexes are unlikely to match # any of existing flavor ids and are guaranteed to be deterministic. for i, server in enumerate(servers): server.flavor['id'] = str(uuid.UUID(int=i)) + api.glance.image_list_detailed(IsA(http.HttpRequest))\ + .AndReturn(images) + api.nova.flavor_list(IsA(http.HttpRequest)).AndReturn(flavors) search_opts = {'marker': None, 'paginate': True} api.nova.server_list(IsA(http.HttpRequest), all_tenants=True, search_opts=search_opts) \ @@ -115,8 +129,6 @@ class InstanceViewTest(test.BaseAdminViewTests): .MultipleTimes().AndReturn(True) api.nova.extension_supported('Shelve', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) - api.nova.flavor_list(IsA(http.HttpRequest)). \ - AndReturn(flavors) api.keystone.tenant_list(IsA(http.HttpRequest)).\ AndReturn([tenants, False]) for server in servers: @@ -133,10 +145,14 @@ class InstanceViewTest(test.BaseAdminViewTests): self.assertMessageCount(res, error=1) self.assertItemsEqual(instances, servers) - @test.create_stubs({api.nova: ('server_list',), - api.keystone: ('tenant_list',)}) + @test.create_stubs({ + api.nova: ('server_list', 'flavor_list',), + api.keystone: ('tenant_list',), + api.glance: ('image_list_detailed',), + }) def test_index_server_list_exception(self): tenants = self.tenants.list() + search_opts = {'marker': None, 'paginate': True} api.nova.server_list(IsA(http.HttpRequest), all_tenants=True, search_opts=search_opts) \ @@ -188,14 +204,21 @@ class InstanceViewTest(test.BaseAdminViewTests): self.assertContains(res, "Active", 1, 200) self.assertContains(res, "Running", 1, 200) - @test.create_stubs({api.nova: ('flavor_list', 'server_list', - 'extension_supported', ), - api.keystone: ('tenant_list',), - api.network: ('servers_update_addresses',)}) + @test.create_stubs({ + api.nova: ('flavor_list', 'server_list', 'extension_supported', ), + api.keystone: ('tenant_list',), + api.network: ('servers_update_addresses',), + api.glance: ('image_list_detailed',), + }) def test_index_options_before_migrate(self): servers = self.servers.list() + images = self.images.list() + flavors = self.flavors.list() api.keystone.tenant_list(IsA(http.HttpRequest)).\ AndReturn([self.tenants.list(), False]) + api.glance.image_list_detailed(IsA(http.HttpRequest)) \ + .AndReturn(images) + api.nova.flavor_list(IsA(http.HttpRequest)).AndReturn(flavors) search_opts = {'marker': None, 'paginate': True} api.nova.server_list(IsA(http.HttpRequest), all_tenants=True, search_opts=search_opts) \ @@ -206,8 +229,6 @@ class InstanceViewTest(test.BaseAdminViewTests): .MultipleTimes().AndReturn(True) api.nova.extension_supported('Shelve', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) - api.nova.flavor_list(IsA(http.HttpRequest)).\ - AndReturn(self.flavors.list()) self.mox.ReplayAll() res = self.client.get(INDEX_URL) @@ -215,18 +236,25 @@ class InstanceViewTest(test.BaseAdminViewTests): self.assertNotContains(res, "instances__confirm") self.assertNotContains(res, "instances__revert") - @test.create_stubs({api.nova: ('flavor_list', 'server_list', - 'extension_supported', ), - api.keystone: ('tenant_list',), - api.network: ('servers_update_addresses',)}) + @test.create_stubs({ + api.nova: ('flavor_list', 'server_list', 'extension_supported',), + api.keystone: ('tenant_list',), + api.network: ('servers_update_addresses',), + api.glance: ('image_list_detailed',), + }) def test_index_options_after_migrate(self): servers = self.servers.list() server1 = servers[0] server1.status = "VERIFY_RESIZE" server2 = servers[2] server2.status = "VERIFY_RESIZE" + images = self.images.list() + flavors = self.flavors.list() api.keystone.tenant_list(IsA(http.HttpRequest)) \ .AndReturn([self.tenants.list(), False]) + api.glance.image_list_detailed(IsA(http.HttpRequest)) \ + .AndReturn(images) + api.nova.flavor_list(IsA(http.HttpRequest)).AndReturn(flavors) search_opts = {'marker': None, 'paginate': True} api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ .MultipleTimes().AndReturn(True) @@ -237,8 +265,6 @@ class InstanceViewTest(test.BaseAdminViewTests): .AndReturn([servers, False]) api.network.servers_update_addresses(IsA(http.HttpRequest), servers, all_tenants=True) - api.nova.flavor_list(IsA(http.HttpRequest)).\ - AndReturn(self.flavors.list()) self.mox.ReplayAll() res = self.client.get(INDEX_URL) diff --git a/openstack_dashboard/dashboards/admin/instances/views.py b/openstack_dashboard/dashboards/admin/instances/views.py index ac3a9f7950..daa29f190b 100644 --- a/openstack_dashboard/dashboards/admin/instances/views.py +++ b/openstack_dashboard/dashboards/admin/instances/views.py @@ -30,6 +30,7 @@ from horizon import tables from horizon.utils import memoized from openstack_dashboard import api + from openstack_dashboard.dashboards.admin.instances \ import forms as project_forms from openstack_dashboard.dashboards.admin.instances \ @@ -59,6 +60,11 @@ def rdp(args, **kvargs): return views.rdp(args, **kvargs) +# re-use get_resource_id_by_name from project.instances.views +def swap_filter(resources, filters, fake_field, real_field): + return views.swap_filter(resources, filters, fake_field, real_field) + + class AdminUpdateView(views.UpdateView): workflow_class = update_instance.AdminUpdateInstance success_url = reverse_lazy("horizon:admin:instances:index") @@ -102,15 +108,32 @@ class AdminIndexView(tables.DataTableView): msg = _('Unable to retrieve instance project information.') exceptions.handle(self.request, msg) - if 'project' in search_opts: - ten_filter_ids = [t.id for t in tenants - if t.name == search_opts['project']] - del search_opts['project'] - if len(ten_filter_ids) > 0: - search_opts['tenant_id'] = ten_filter_ids[0] - else: + # Gather our images to correlate againts IDs + try: + images = api.glance.image_list_detailed(self.request)[0] + except Exception: + images = [] + msg = _("Unable to retrieve image list.") + + # Gather our flavors to correlate against IDs + try: + flavors = api.nova.flavor_list(self.request) + except Exception: + # If fails to retrieve flavor list, creates an empty list. + flavors = [] + + if 'project' in search_opts and \ + not swap_filter(tenants, search_opts, 'project', 'tenant_id'): self._more = False - return [] + return instances + elif 'image_name' in search_opts and \ + not swap_filter(images, search_opts, 'image_name', 'image'): + self._more = False + return instances + elif "flavor_name" in search_opts and \ + not swap_filter(flavors, search_opts, 'flavor_name', 'flavor'): + self._more = False + return instances try: instances, self._more = api.nova.server_list( @@ -131,13 +154,6 @@ class AdminIndexView(tables.DataTableView): message=_('Unable to retrieve IP addresses from Neutron.'), ignore=True) - # Gather our flavors to correlate against IDs - try: - flavors = api.nova.flavor_list(self.request) - except Exception: - # If fails to retrieve flavor list, creates an empty list. - flavors = [] - full_flavors = OrderedDict([(f.id, f) for f in flavors]) tenant_dict = OrderedDict([(t.id, t) for t in tenants]) # Loop through instances to get flavor and tenant info. diff --git a/openstack_dashboard/dashboards/project/instances/tables.py b/openstack_dashboard/dashboards/project/instances/tables.py index 3d0edd1228..5dfd54e3bd 100644 --- a/openstack_dashboard/dashboards/project/instances/tables.py +++ b/openstack_dashboard/dashboards/project/instances/tables.py @@ -1171,13 +1171,27 @@ POWER_DISPLAY_CHOICES = ( ("BUILDING", pgettext_lazy("Power state of an Instance", u"Building")), ) +INSTANCE_FILTER_CHOICES = ( + ('uuid', _("Instance ID ="), True), + ('name', _("Instance Name"), True), + ('image', _("Image ID ="), True), + ('image_name', _("Image Name ="), True), + ('ip', _("IPv4 Address"), True), + ('ip6', _("IPv6 Address"), True), + ('flavor', _("Flavor ID ="), True), + ('flavor_name', _("Flavor Name ="), True), + ('key_name', _("Key Pair Name"), True), + ('status', _("Status ="), True), + ('availability_zone', _("Availability Zone"), True), + ('changes-since', _("Changes Since"), True, + _("Filter by an ISO 8061 formatted time, e.g. 2016-06-14T06:27:59Z")), + ('vcpus', _("vCPUs ="), True), +) + class InstancesFilterAction(tables.FilterAction): filter_type = "server" - filter_choices = (('name', _("Instance Name ="), True), - ('status', _("Status ="), True), - ('image', _("Image ID ="), True), - ('flavor', _("Flavor ID ="), True)) + filter_choices = INSTANCE_FILTER_CHOICES class InstancesTable(tables.DataTable): diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index 6283f9059a..2b8cbbc887 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -106,10 +106,18 @@ class InstanceTests(helpers.TestCase): self.assertItemsEqual(instances, self.servers.list()) self.assertNotContains(res, "Launch Instance (Quota exceeded)") - @helpers.create_stubs({api.nova: ('server_list', - 'tenant_absolute_limits',)}) + @helpers.create_stubs({ + api.nova: ('server_list', 'tenant_absolute_limits', 'flavor_list'), + api.glance: ('image_list_detailed',), + }) def test_index_server_list_exception(self): search_opts = {'marker': None, 'paginate': True} + flavors = self.flavors.list() + images = self.images.list() + api.nova.flavor_list(IsA(http.HttpRequest)) \ + .AndReturn(flavors) + api.glance.image_list_detailed(IsA(http.HttpRequest)) \ + .AndReturn(images) api.nova.server_list(IsA(http.HttpRequest), search_opts=search_opts) \ .AndRaise(self.exceptions.nova) api.nova.tenant_absolute_limits(IsA(http.HttpRequest), reserved=True) \ diff --git a/openstack_dashboard/dashboards/project/instances/views.py b/openstack_dashboard/dashboards/project/instances/views.py index 3f798084aa..5a7a4217d5 100644 --- a/openstack_dashboard/dashboards/project/instances/views.py +++ b/openstack_dashboard/dashboards/project/instances/views.py @@ -64,9 +64,35 @@ class IndexView(tables.DataTableView): return self._more def get_data(self): + instances = [] marker = self.request.GET.get( project_tables.InstancesTable._meta.pagination_param, None) + search_opts = self.get_filters({'marker': marker, 'paginate': True}) + + # Gather our flavors and images and correlate our instances to them + try: + flavors = api.nova.flavor_list(self.request) + except Exception: + flavors = [] + exceptions.handle(self.request, ignore=True) + + try: + # TODO(gabriel): Handle pagination. + images = api.glance.image_list_detailed(self.request)[0] + except Exception: + images = [] + exceptions.handle(self.request, ignore=True) + + if 'image_name' in search_opts and \ + not swap_filter(images, search_opts, 'image_name', 'image'): + self._more = False + return instances + elif 'flavor_name' in search_opts and \ + not swap_filter(flavors, search_opts, 'flavor_name', 'flavor'): + self._more = False + return instances + # Gather our instances try: instances, self._more = api.nova.server_list( @@ -86,22 +112,6 @@ class IndexView(tables.DataTableView): self.request, message=_('Unable to retrieve IP addresses from Neutron.'), ignore=True) - - # Gather our flavors and images and correlate our instances to them - try: - flavors = api.nova.flavor_list(self.request) - except Exception: - flavors = [] - exceptions.handle(self.request, ignore=True) - - try: - # TODO(gabriel): Handle pagination. - images, more, prev = api.glance.image_list_detailed( - self.request) - except Exception: - images = [] - exceptions.handle(self.request, ignore=True) - full_flavors = OrderedDict([(str(flavor.id), flavor) for flavor in flavors]) image_map = OrderedDict([(str(image.id), image) @@ -114,7 +124,6 @@ class IndexView(tables.DataTableView): if isinstance(instance.image, dict): if instance.image.get('id') in image_map: instance.image = image_map[instance.image['id']] - try: flavor_id = instance.flavor["id"] if flavor_id in full_flavors: @@ -131,6 +140,16 @@ class IndexView(tables.DataTableView): return instances +def swap_filter(resources, filters, fake_field, real_field): + if fake_field in filters: + filter_string = filters[fake_field] + for resource in resources: + if resource.name.lower() == filter_string.lower(): + filters[real_field] = resource.id + del filters[fake_field] + return True + + class LaunchInstanceView(workflows.WorkflowView): workflow_class = project_workflows.LaunchInstance