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
This commit is contained in:
Eddie Ramirez 2016-06-22 16:55:05 +00:00 committed by David Lyle
parent 110175a549
commit 460a53f96d
6 changed files with 156 additions and 76 deletions

View File

@ -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):

View File

@ -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)

View File

@ -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.

View File

@ -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):

View File

@ -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) \

View File

@ -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