From 9f9d4ab62b48358956fba2c8a2e7cc1672d7017d Mon Sep 17 00:00:00 2001 From: Gabriel Hurley Date: Wed, 18 Jan 2012 11:25:55 -0800 Subject: [PATCH] Converts images and snapshots to new tables, modals, error handling, etc. Fixed bug 905235. Fixed bug 906487. Change-Id: Ida68d82508357fe31695c0c66f7aaeabdc911105 --- .../dashboards/nova/containers/forms.py | 2 - .../nova/images_and_snapshots/images/forms.py | 176 +++++---------- .../images_and_snapshots/images/tables.py | 81 +++++++ .../nova/images_and_snapshots/images/tests.py | 117 ++++------ .../nova/images_and_snapshots/images/urls.py | 7 +- .../nova/images_and_snapshots/images/views.py | 210 ++++++++---------- .../images_and_snapshots/snapshots/tables.py | 43 ++++ .../nova/images_and_snapshots/tests.py | 77 ++----- .../nova/images_and_snapshots/urls.py | 12 +- .../nova/images_and_snapshots/views.py | 57 ++--- .../images_and_snapshots/images/_delete.html | 9 - .../images_and_snapshots/images/_launch.html | 9 +- .../images_and_snapshots/images/_list.html | 52 ----- .../images_and_snapshots/images/_update.html | 2 +- .../images_and_snapshots/images/update.html | 2 +- .../nova/images_and_snapshots/index.html | 26 +-- .../snapshots/_create.html | 2 +- .../images_and_snapshots/snapshots/_list.html | 53 ----- .../images_and_snapshots/snapshots/index.html | 23 -- .../instances/_no_instances.html | 2 +- horizon/horizon/exceptions.py | 18 +- horizon/horizon/middleware.py | 2 + horizon/horizon/tables/base.py | 5 +- horizon/horizon/templatetags/launch_form.py | 78 ------- horizon/horizon/tests/table_tests.py | 1 - 25 files changed, 400 insertions(+), 666 deletions(-) create mode 100644 horizon/horizon/dashboards/nova/images_and_snapshots/images/tables.py create mode 100644 horizon/horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py delete mode 100644 horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_delete.html delete mode 100644 horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_list.html delete mode 100644 horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/snapshots/_list.html delete mode 100644 horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/snapshots/index.html delete mode 100644 horizon/horizon/templatetags/launch_form.py diff --git a/horizon/horizon/dashboards/nova/containers/forms.py b/horizon/horizon/dashboards/nova/containers/forms.py index b9b8deba84f..29006a1f007 100644 --- a/horizon/horizon/dashboards/nova/containers/forms.py +++ b/horizon/horizon/dashboards/nova/containers/forms.py @@ -69,9 +69,7 @@ class CopyObject(forms.SelfHandlingForm): def __init__(self, *args, **kwargs): containers = kwargs.pop('containers') - super(CopyObject, self).__init__(*args, **kwargs) - self.fields['new_container_name'].choices = containers def handle(self, request, data): diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/images/forms.py b/horizon/horizon/dashboards/nova/images_and_snapshots/images/forms.py index 3f7e6c9192c..c18041efc32 100644 --- a/horizon/horizon/dashboards/nova/images_and_snapshots/images/forms.py +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/images/forms.py @@ -24,8 +24,9 @@ import logging +from django import shortcuts from django.contrib import messages -from django.shortcuts import redirect +from django.core.urlresolvers import reverse from django.utils.text import normalize_newlines from django.utils.translation import ugettext as _ from glance.common import exception as glance_exception @@ -50,57 +51,34 @@ class UpdateImageForm(forms.SelfHandlingForm): disk_format = forms.CharField(label=_("Disk Format")) def handle(self, request, data): + # TODO add public flag to image meta properties image_id = data['image_id'] - tenant_id = request.user.tenant_id - error_retrieving = _('Unable to retrieve image info from glance: %s' - % image_id) - error_updating = _('Error updating image with id: %s' % image_id) + error_retrieving = _('Unable to retrieve image "%s".') + error_updating = _('Unable to update image "%s".') try: image = api.image_get_meta(request, image_id) - except glance_exception.ClientConnectionError, e: - LOG.exception(_('Error connecting to glance')) - messages.error(request, error_retrieving) - except glance_exception.Error, e: - LOG.exception(error_retrieving) - messages.error(request, error_retrieving) - - if image.owner == request.user.tenant_id: - try: - meta = { - 'is_public': True, - 'disk_format': data['disk_format'], - 'container_format': data['container_format'], - 'name': data['name'], - } - # TODO add public flag to properties - meta['properties'] = {} - if data['kernel']: - meta['properties']['kernel_id'] = data['kernel'] - - if data['ramdisk']: - meta['properties']['ramdisk_id'] = data['ramdisk'] - - if data['architecture']: - meta['properties']['architecture'] = data['architecture'] - - api.image_update(request, image_id, meta) - messages.success(request, _('Image was successfully updated.')) - - except glance_exception.ClientConnectionError, e: - LOG.exception(_('Error connecting to glance')) - messages.error(request, error_retrieving) - except glance_exception.Error, e: - LOG.exception(error_updating) - messages.error(request, error_updating) - except: - LOG.exception(_('Unspecified Exception in image update')) - messages.error(request, error_updating) - return redirect('dash_images_update', tenant_id, image_id) - else: - messages.info(request, _('Unable to delete the requested image ' - 'because you are not its owner.')) - return redirect('dash_images_update', tenant_id, image_id) + except: + exceptions.handle(request, error_retrieving % image_id) + + meta = {'is_public': True, + 'disk_format': data['disk_format'], + 'container_format': data['container_format'], + 'name': data['name'], + 'properties': {}} + if data['kernel']: + meta['properties']['kernel_id'] = data['kernel'] + if data['ramdisk']: + meta['properties']['ramdisk_id'] = data['ramdisk'] + if data['architecture']: + meta['properties']['architecture'] = data['architecture'] + + try: + api.image_update(request, image_id, meta) + messages.success(request, _('Image was successfully updated.')) + except: + exceptions.handle(request, error_updating % image_id) + return shortcuts.redirect('horizon:nova:images_and_snapshots:index') class LaunchForm(forms.SelfHandlingForm): @@ -110,87 +88,43 @@ class LaunchForm(forms.SelfHandlingForm): user_data = forms.CharField(widget=forms.Textarea, label=_("User Data"), required=False) + flavor = forms.ChoiceField(label=_("Flavor"), + help_text=_("Size of image to launch.")) + keypair = forms.ChoiceField(label=_("Keypair"), + required=False, + help_text=_("Which keypair to use for " + "authentication.")) + security_groups = forms.MultipleChoiceField( + label=_("Security Groups"), + required=True, + initial=["default"], + widget=forms.CheckboxSelectMultiple(), + help_text=_("Launch instance in these " + "security groups.")) - # make the dropdown populate when the form is loaded not when django is - # started def __init__(self, *args, **kwargs): + flavor_list = kwargs.pop('flavor_list') + keypair_list = kwargs.pop('keypair_list') + security_group_list = kwargs.pop('security_group_list') super(LaunchForm, self).__init__(*args, **kwargs) - flavorlist = kwargs.get('initial', {}).get('flavorlist', []) - self.fields['flavor'] = forms.ChoiceField( - choices=flavorlist, - label=_("Flavor"), - help_text="Size of Image to launch") - - keynamelist = kwargs.get('initial', {}).get('keynamelist', []) - self.fields['key_name'] = forms.ChoiceField(choices=keynamelist, - label=_("Key Name"), - required=False, - help_text="Which keypair to use for authentication") - - securitygrouplist = kwargs.get('initial', {}).get( - 'securitygrouplist', []) - self.fields['security_groups'] = forms.MultipleChoiceField( - choices=securitygrouplist, - label=_("Security Groups"), - required=True, - initial=['default'], - widget=forms.CheckboxSelectMultiple(), - help_text="Launch instance in these Security Groups") - # setting self.fields.keyOrder seems to break validation, - # so ordering fields manually - field_list = ( - 'name', - 'user_data', - 'flavor', - 'key_name') - for field in field_list[::-1]: - self.fields.insert(0, field, self.fields.pop(field)) + self.fields['flavor'].choices = flavor_list + self.fields['keypair'].choices = keypair_list + self.fields['security_groups'].choices = security_group_list def handle(self, request, data): - image_id = data['image_id'] - tenant_id = data['tenant_id'] try: - image = api.image_get_meta(request, image_id) - flavor = api.flavor_get(request, data['flavor']) api.server_create(request, data['name'], - image, - flavor, - data.get('key_name'), + data['image_id'], + data['flavor'], + data.get('keypair'), normalize_newlines(data.get('user_data')), data.get('security_groups')) - - msg = _('Instance was successfully launched') - LOG.info(msg) - messages.success(request, msg) - return redirect( - 'horizon:nova:instances_and_volumes:index') - + messages.success(request, + _('Instance "%s" launched.') % data["name"]) except: - exceptions.handle(request, _('Unable to launch instance.')) - - -class DeleteImage(forms.SelfHandlingForm): - image_id = forms.CharField(required=True) - - def handle(self, request, data): - image_id = data['image_id'] - tenant_id = request.user.tenant_id - try: - image = api.image_get_meta(request, image_id) - if image.owner == request.user.username: - api.image_delete(request, image_id) - else: - messages.info(request, _("Unable to delete image, you are not \ - its owner.")) - return redirect('dash_images_update', tenant_id, image_id) - except glance_exception.ClientConnectionError, e: - LOG.exception("Error connecting to glance") - messages.error(request, _("Error connecting to glance: %s") - % e.message) - except glance_exception.Error, e: - LOG.exception('Error deleting image with id "%s"' % image_id) - messages.error(request, - _("Error deleting image: %(image)s: %i(msg)s") - % {"image": image_id, "msg": e.message}) - return redirect(request.build_absolute_uri()) + redirect = reverse("horizon:nova:images_and_snapshots:index") + exceptions.handle(request, + _('Unable to launch instance: %(exc)s'), + redirect=redirect) + return shortcuts.redirect('horizon:nova:instances_and_volumes:index') diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/images/tables.py b/horizon/horizon/dashboards/nova/images_and_snapshots/images/tables.py new file mode 100644 index 00000000000..116db4afa4a --- /dev/null +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/images/tables.py @@ -0,0 +1,81 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Nebula, Inc. +# +# 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. + +import logging + +from django import shortcuts +from django.contrib import messages +from django.core.urlresolvers import reverse +from django.template import defaultfilters as filters +from novaclient import exceptions as novaclient_exceptions + +from horizon import api +from horizon import tables + + +LOG = logging.getLogger(__name__) + + +class DeleteImage(tables.DeleteAction): + data_type_singular = _("Image") + data_type_plural = _("Images") + + def allowed(self, request, image=None): + if image: + return image.owner == request.user.id + return True + + def delete(self, request, obj_id): + api.image_delete(request, obj_id) + + +class LaunchImage(tables.LinkAction): + name = "launch" + verbose_name = _("Launch") + url = "horizon:nova:images_and_snapshots:images:launch" + attrs = {"class": "ajax-modal"} + + +class EditImage(tables.LinkAction): + name = "edit" + verbose_name = _("Edit") + url = "horizon:nova:images_and_snapshots:images:update" + attrs = {"class": "ajax-modal"} + + +def get_image_type(image): + return getattr(image.properties, "image_type", "Image") + + +class ImagesTable(tables.DataTable): + name = tables.Column("name") + image_type = tables.Column(get_image_type, + verbose_name=_("Type"), + filters=(filters.title,)) + status = tables.Column("status", filters=(filters.title,)) + public = tables.Column("is_public", + verbose_name=_("Public"), + empty_value=False, + filters=(filters.yesno, filters.capfirst)) + container_format = tables.Column("container_format", + verbose_name=_("Container Format"), + filters=(unicode.upper,)) + + class Meta: + name = "images" + verbose_name = _("Images") + table_actions = (DeleteImage,) + row_actions = (LaunchImage, EditImage, DeleteImage) diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/images/tests.py b/horizon/horizon/dashboards/nova/images_and_snapshots/images/tests.py index cbe7dbf90de..8a191bb5523 100644 --- a/horizon/horizon/dashboards/nova/images_and_snapshots/images/tests.py +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/images/tests.py @@ -42,12 +42,12 @@ def setUp(self): image_dict = {'name': 'visibleImage', 'container_format': 'novaImage'} self.visibleImage = api.Image(image_dict) - self.visibleImage.id = '1' + self.visibleImage.id = 1 image_dict = {'name': 'invisibleImage', 'container_format': 'aki'} self.invisibleImage = api.Image(image_dict) - self.invisibleImage.id = '2' + self.invisibleImage.id = 2 self.images = (self.visibleImage, self.invisibleImage) @@ -68,23 +68,24 @@ def setUp(self): self.security_groups = (security_group,) def test_launch_get(self): - IMAGE_ID = '1' + IMAGE_ID = 1 self.mox.StubOutWithMock(api, 'image_get_meta') - api.image_get_meta(IsA(http.HttpRequest), - IMAGE_ID).AndReturn(self.visibleImage) - self.mox.StubOutWithMock(api, 'tenant_quota_get') + self.mox.StubOutWithMock(api, 'flavor_list') + self.mox.StubOutWithMock(api, 'keypair_list') + self.mox.StubOutWithMock(api, 'security_group_list') + + api.image_get_meta(IsA(http.HttpRequest), str(IMAGE_ID)) \ + .AndReturn(self.visibleImage) + api.tenant_quota_get(IsA(http.HttpRequest), self.TEST_TENANT).AndReturn(FakeQuota) - self.mox.StubOutWithMock(api, 'flavor_list') api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors) - self.mox.StubOutWithMock(api, 'keypair_list') api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) - self.mox.StubOutWithMock(api, 'security_group_list') api.security_group_list(IsA(http.HttpRequest)).AndReturn( self.security_groups) @@ -93,33 +94,26 @@ def test_launch_get(self): res = self.client.get( reverse('horizon:nova:images_and_snapshots:images:launch', args=[IMAGE_ID])) + form = res.context['form'] self.assertTemplateUsed(res, 'nova/images_and_snapshots/images/launch.html') - - image = res.context['image'] - self.assertEqual(image.name, self.visibleImage.name) - - form = res.context['form'] - - form_flavorfield = form.fields['flavor'] - self.assertIn('m1.massive', form_flavorfield.choices[0][1]) - - form_keyfield = form.fields['key_name'] - self.assertEqual(form_keyfield.choices[0][0], + self.assertEqual(res.context['image'].name, self.visibleImage.name) + self.assertIn('m1.massive', form.fields['flavor'].choices[0][1]) + self.assertEqual(form.fields['keypair'].choices[0][0], self.keypairs[0].name) def test_launch_post(self): FLAVOR_ID = self.flavors[0].id IMAGE_ID = '1' - KEY_NAME = self.keypairs[0].name + keypair = self.keypairs[0].name SERVER_NAME = 'serverName' USER_DATA = 'userData' form_data = {'method': 'LaunchForm', 'flavor': FLAVOR_ID, 'image_id': IMAGE_ID, - 'key_name': KEY_NAME, + 'keypair': keypair, 'name': SERVER_NAME, 'user_data': USER_DATA, 'tenant_id': self.TEST_TENANT, @@ -127,39 +121,20 @@ def test_launch_post(self): } self.mox.StubOutWithMock(api, 'image_get_meta') - api.image_get_meta(IsA(http.HttpRequest), - IMAGE_ID).AndReturn(self.visibleImage) - - self.mox.StubOutWithMock(api, 'tenant_quota_get') - api.tenant_quota_get(IsA(http.HttpRequest), - self.TEST_TENANT).AndReturn(FakeQuota) - self.mox.StubOutWithMock(api, 'flavor_list') - api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors) - self.mox.StubOutWithMock(api, 'keypair_list') - api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) - self.mox.StubOutWithMock(api, 'security_group_list') + self.mox.StubOutWithMock(api, 'server_create') + + api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors) + api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) api.security_group_list(IsA(http.HttpRequest)).AndReturn( self.security_groups) - - # called again by the form api.image_get_meta(IsA(http.HttpRequest), IMAGE_ID).AndReturn(self.visibleImage) - - self.mox.StubOutWithMock(api, 'flavor_get') - api.flavor_get(IsA(http.HttpRequest), - IsA(unicode)).AndReturn(self.flavors[0]) - - self.mox.StubOutWithMock(api, 'server_create') - api.server_create(IsA(http.HttpRequest), SERVER_NAME, - self.visibleImage, self.flavors[0], - KEY_NAME, USER_DATA, [self.security_groups[0].name]) - - self.mox.StubOutWithMock(messages, 'success') - messages.success(IsA(http.HttpRequest), IsA(basestring)) + str(IMAGE_ID), str(FLAVOR_ID), + keypair, USER_DATA, [self.security_groups[0].name]) self.mox.ReplayAll() @@ -175,21 +150,18 @@ def test_launch_flavorlist_error(self): IMAGE_ID = '1' self.mox.StubOutWithMock(api, 'image_get_meta') + self.mox.StubOutWithMock(api, 'tenant_quota_get') + self.mox.StubOutWithMock(api, 'flavor_list') + self.mox.StubOutWithMock(api, 'keypair_list') + self.mox.StubOutWithMock(api, 'security_group_list') + api.image_get_meta(IsA(http.HttpRequest), IMAGE_ID).AndReturn(self.visibleImage) - - self.mox.StubOutWithMock(api, 'tenant_quota_get') api.tenant_quota_get(IsA(http.HttpRequest), self.TEST_TENANT).AndReturn(FakeQuota) - exception = keystone_exceptions.ClientException('Failed.') - self.mox.StubOutWithMock(api, 'flavor_list') api.flavor_list(IsA(http.HttpRequest)).AndRaise(exception) - - self.mox.StubOutWithMock(api, 'keypair_list') api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) - - self.mox.StubOutWithMock(api, 'security_group_list') api.security_group_list(IsA(http.HttpRequest)).AndReturn( self.security_groups) @@ -202,11 +174,6 @@ def test_launch_flavorlist_error(self): self.assertTemplateUsed(res, 'nova/images_and_snapshots/images/launch.html') - form = res.context['form'] - - form_flavorfield = form.fields['flavor'] - self.assertIn('m1.tiny', form_flavorfield.choices[0][1]) - def test_launch_keypairlist_error(self): IMAGE_ID = '2' @@ -240,52 +207,45 @@ def test_launch_keypairlist_error(self): form = res.context['form'] - form_keyfield = form.fields['key_name'] + form_keyfield = form.fields['keypair'] self.assertEqual(len(form_keyfield.choices), 0) def test_launch_form_keystone_exception(self): FLAVOR_ID = self.flavors[0].id IMAGE_ID = '1' - KEY_NAME = self.keypairs[0].name + keypair = self.keypairs[0].name SERVER_NAME = 'serverName' USER_DATA = 'userData' self.mox.StubOutWithMock(api, 'image_get_meta') - self.mox.StubOutWithMock(api, 'tenant_quota_get') self.mox.StubOutWithMock(api, 'flavor_list') self.mox.StubOutWithMock(api, 'keypair_list') self.mox.StubOutWithMock(api, 'security_group_list') - self.mox.StubOutWithMock(api, 'flavor_get') self.mox.StubOutWithMock(api, 'server_create') form_data = {'method': 'LaunchForm', 'flavor': FLAVOR_ID, 'image_id': IMAGE_ID, - 'key_name': KEY_NAME, + 'keypair': keypair, 'name': SERVER_NAME, 'tenant_id': self.TEST_TENANT, 'user_data': USER_DATA, - 'security_groups': 'default', - } + 'security_groups': 'default'} - api.image_get_meta(IgnoreArg(), - IMAGE_ID).AndReturn(self.visibleImage) - api.tenant_quota_get(IsA(http.HttpRequest), - self.TEST_TENANT).AndReturn(FakeQuota) api.flavor_list(IgnoreArg()).AndReturn(self.flavors) api.keypair_list(IgnoreArg()).AndReturn(self.keypairs) api.security_group_list(IsA(http.HttpRequest)).AndReturn( self.security_groups) - # called again by the form api.image_get_meta(IgnoreArg(), IMAGE_ID).AndReturn(self.visibleImage) - api.flavor_get(IgnoreArg(), - IsA(unicode)).AndReturn(self.flavors[0]) exception = keystone_exceptions.ClientException('Failed') - api.server_create(IsA(http.HttpRequest), SERVER_NAME, - self.visibleImage, self.flavors[0], - KEY_NAME, USER_DATA, + api.server_create(IsA(http.HttpRequest), + SERVER_NAME, + IMAGE_ID, + str(FLAVOR_ID), + keypair, + USER_DATA, [group.name for group in self.security_groups]) \ .AndRaise(exception) @@ -297,5 +257,4 @@ def test_launch_form_keystone_exception(self): args=[IMAGE_ID]) res = self.client.post(url, form_data) - self.assertTemplateUsed(res, - 'nova/images_and_snapshots/images/launch.html') + self.assertRedirectsNoFollow(res, IMAGES_INDEX_URL) diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/images/urls.py b/horizon/horizon/dashboards/nova/images_and_snapshots/images/urls.py index bdb75ea5589..2a73cd01f5d 100644 --- a/horizon/horizon/dashboards/nova/images_and_snapshots/images/urls.py +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/images/urls.py @@ -20,11 +20,12 @@ from django.conf.urls.defaults import patterns, url +from .views import UpdateView, LaunchView VIEWS_MOD = 'horizon.dashboards.nova.images_and_snapshots.images.views' urlpatterns = patterns(VIEWS_MOD, - url(r'^$', 'index', name='index'), - url(r'^(?P[^/]+)/launch/$', 'launch', name='launch'), - url(r'^(?P[^/]+)/update/$', 'update', name='update')) + url(r'^(?P[^/]+)/launch/$', LaunchView.as_view(), name='launch'), + url(r'^(?P[^/]+)/update/$', UpdateView.as_view(), name='update') +) diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/images/views.py b/horizon/horizon/dashboards/nova/images_and_snapshots/images/views.py index a2f51808dbe..e6eaeee7ebc 100644 --- a/horizon/horizon/dashboards/nova/images_and_snapshots/images/views.py +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/images/views.py @@ -26,6 +26,7 @@ from django import shortcuts from django.contrib import messages +from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ from glance.common import exception as glance_exception from novaclient import exceptions as novaclient_exceptions @@ -33,135 +34,106 @@ from horizon import api from horizon import exceptions -from .forms import UpdateImageForm, LaunchForm, DeleteImage +from horizon import forms +from .forms import UpdateImageForm, LaunchForm LOG = logging.getLogger(__name__) -def index(request): - for f in (DeleteImage, ): - unused, handled = f.maybe_handle(request) - if handled: - return handled - all_images = [] - try: - all_images = api.image_list_detailed(request) - if not all_images: - messages.info(request, _("There are currently no images.")) - except glance_exception.ClientConnectionError, e: - LOG.exception("Error connecting to glance") - messages.error(request, _("Error connecting to glance: %s") % str(e)) - except glance_exception.GlanceException, e: - LOG.exception("Error retrieving image list") - messages.error(request, _("Error retrieving image list: %s") % str(e)) - except Exception, e: - msg = _("Unable to retrieve image info from glance: %s") % str(e) - LOG.exception(msg) - messages.error(request, msg) - - images = [im for im in all_images - if im['container_format'] not in ['aki', 'ari']] - - context = {'delete_form': DeleteImage(), 'images': images} - - return shortcuts.render(request, - 'nova/images_and_snapshots/images/index.html', { - 'delete_form': DeleteImage(), - 'quotas': quotas, - 'images': images}) - - -def launch(request, image_id): - def flavorlist(): +class LaunchView(forms.ModalFormView): + form_class = LaunchForm + template_name = 'nova/images_and_snapshots/images/launch.html' + context_object_name = 'image' + + def get_form_kwargs(self): + kwargs = super(LaunchView, self).get_form_kwargs() + kwargs['flavor_list'] = self.flavor_list() + kwargs['keypair_list'] = self.keypair_list() + kwargs['security_group_list'] = self.security_group_list() + return kwargs + + def get_object(self, *args, **kwargs): + image_id = self.kwargs["image_id"] + try: + self.object = api.image_get_meta(self.request, image_id) + except: + msg = _('Unable to retrieve image "%s".') % image_id + redirect = reverse('horizon:nova:images_and_snapshots:index') + exceptions.handle(self.request, msg, redirect=redirect) + return self.object + + def get_context_data(self, **kwargs): + context = super(LaunchView, self).get_context_data(**kwargs) + tenant_id = self.request.user.tenant_id try: - fl = api.flavor_list(request) + quotas = api.tenant_quota_get(self.request, tenant_id) + quotas.ram = int(quotas.ram) + context['quotas'] = quotas + except: + exceptions.handle(self.request) + return context + + def get_initial(self): + return {'image_id': self.kwargs["image_id"], + 'tenant_id': self.request.user.tenant_id} - # TODO add vcpu count to flavors - sel = [(f.id, '%s (%svcpu / %sGB Disk / %sMB Ram )' % - (f.name, f.vcpus, f.disk, f.ram)) for f in fl] - return sorted(sel) + def flavor_list(self): + display = '%(name)s (%(vcpus)sVCPU / %(disk)sGB Disk / %(ram)sMB Ram )' + try: + flavors = api.flavor_list(self.request) + flavor_list = [(flavor.id, display % {"name": flavor.name, + "vcpus": flavor.vcpus, + "disk": flavor.disk, + "ram": flavor.ram}) + for flavor in flavors] except: - exceptions.handle(request, - _('Unable to retrieve list of instance types')) - return [(1, 'm1.tiny')] + flavor_list = [] + exceptions.handle(self.request, + _('Unable to retrieve instance flavors.')) + return sorted(flavor_list) - def keynamelist(): + def keypair_list(self): try: - fl = api.keypair_list(request) - sel = [(f.name, f.name) for f in fl] - return sel + keypairs = api.keypair_list(self.request) + keypair_list = [(kp.name, kp.name) for kp in keypairs] except: - exceptions.handle(request, - _('Unable to retrieve list of keypairs')) - return [] + keypair_list = [] + exceptions.handle(self.request, + _('Unable to retrieve keypairs.')) + return keypair_list - def securitygrouplist(): + def security_group_list(self): try: - fl = api.security_group_list(request) - sel = [(f.name, f.name) for f in fl] - return sel - except novaclient_exceptions.ClientException, e: - LOG.exception('Unable to retrieve list of security groups') - return [] - - tenant_id = request.user.tenant_id - # TODO(mgius): Any reason why these can't be after the launchform logic? - # If The form is valid, we've just wasted these two api calls - image = api.image_get_meta(request, image_id) - quotas = api.tenant_quota_get(request, request.user.tenant_id) - try: - quotas.ram = int(quotas.ram) - except Exception, e: - if not hasattr(e, 'message'): - e.message = str(e) - messages.error(request, - _('Error parsing quota for %(image)s: %(msg)s') % - {"image": image_id, "msg": e.message}) - return shortcuts.redirect( - 'horizon:nova:instances_and_volumes:instances:index') - - form, handled = LaunchForm.maybe_handle( - request, initial={'flavorlist': flavorlist(), - 'keynamelist': keynamelist(), - 'securitygrouplist': securitygrouplist(), - 'image_id': image_id, - 'tenant_id': tenant_id}) - if handled: - return handled - - return shortcuts.render(request, - 'nova/images_and_snapshots/images/launch.html', { - 'image': image, - 'form': form, - 'quotas': quotas}) - - -def update(request, image_id): - try: - image = api.image_get_meta(request, image_id) - except glance_exception.ClientConnectionError, e: - LOG.exception("Error connecting to glance") - messages.error(request, _("Error connecting to glance: %s") - % e.message) - except glance_exception.Error, e: - LOG.exception('Error retrieving image with id "%s"' % image_id) - messages.error(request, - _("Error retrieving image %(image)s: %(msg)s") - % {"image": image_id, "msg": e.message}) - - form, handled = UpdateImageForm().maybe_handle(request, initial={ - 'image_id': image_id, - 'name': image.get('name', ''), - 'kernel': image['properties'].get('kernel_id', ''), - 'ramdisk': image['properties'].get('ramdisk_id', ''), - 'architecture': image['properties'].get('architecture', ''), - 'container_format': image.get('container_format', ''), - 'disk_format': image.get('disk_format', ''), }) - if handled: - return handled - - context = {'form': form, "image": image} - template = 'nova/images_and_snapshots/images/update.html' - - return shortcuts.render(request, template, context) + groups = api.security_group_list(self.request) + security_group_list = [(sg.name, sg.name) for sg in groups] + except: + exceptions.handle(self.request, + _('Unable to retrieve list of security groups')) + security_group_list = [] + return security_group_list + + +class UpdateView(forms.ModalFormView): + form_class = UpdateImageForm + template_name = 'nova/images_and_snapshots/images/update.html' + context_object_name = 'image' + + def get_object(self, *args, **kwargs): + try: + self.object = api.image_get_meta(self.request, kwargs['image_id']) + except: + msg = _('Unable to retrieve image "%s".') % kwargs['image_id'] + redirect = reverse('horizon:nova:images_and_snapshots:index') + exceptions.handle(self.request, msg, redirect=redirect) + return self.object + + def get_initial(self): + properties = self.object['properties'] + return {'image_id': self.kwargs['image_id'], + 'name': self.object.get('name', ''), + 'kernel': properties.get('kernel_id', ''), + 'ramdisk': properties.get('ramdisk_id', ''), + 'architecture': properties.get('architecture', ''), + 'container_format': self.object.get('container_format', ''), + 'disk_format': self.object.get('disk_format', ''), } diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py b/horizon/horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py new file mode 100644 index 00000000000..5b18672bf62 --- /dev/null +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/snapshots/tables.py @@ -0,0 +1,43 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Nebula, Inc. +# +# 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. + +import logging + +from django import shortcuts +from django.contrib import messages +from django.core.urlresolvers import reverse +from django.template import defaultfilters as filters +from novaclient import exceptions as novaclient_exceptions + +from horizon import api +from horizon import tables +from ..images.tables import ImagesTable, LaunchImage, EditImage, DeleteImage + + +LOG = logging.getLogger(__name__) + + +class DeleteSnapshot(DeleteImage): + data_type_singular = _("Snapshot") + data_type_plural = _("Snapshots") + + +class SnapshotsTable(ImagesTable): + class Meta: + name = "snapshots" + verbose_name = _("Snapshots") + table_actions = (DeleteSnapshot,) + row_actions = (LaunchImage, EditImage, DeleteSnapshot) diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/tests.py b/horizon/horizon/dashboards/nova/images_and_snapshots/tests.py index 80a10467915..447b1381e19 100644 --- a/horizon/horizon/dashboards/nova/images_and_snapshots/tests.py +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/tests.py @@ -35,16 +35,24 @@ class ImagesAndSnapshotsTests(test.BaseViewTests): def setUp(self): super(ImagesAndSnapshotsTests, self).setUp() - snapshot_dict = {'name': 'snapshot', - 'container_format': 'ami', + snapshot_properties = api.glance.ImageProperties(None) + snapshot_properties.image_type = u'snapshot' + + snapshot_dict = {'name': u'snapshot', + 'container_format': u'ami', 'id': 3} - snapshot = api.Image(snapshot_dict) - self.snapshots = [snapshot, ] + snapshot = api.glance.Image(snapshot_dict) + snapshot.properties = snapshot_properties + self.snapshots = [snapshot] + + image_properties = api.glance.ImageProperties(None) + image_properties.image_type = u'image' - image_dict = {'name': 'visibleImage', - 'container_format': 'novaImage'} - self.visibleImage = api.Image(image_dict) + image_dict = {'name': u'visibleImage', + 'container_format': u'novaImage'} + self.visibleImage = api.glance.Image(image_dict) self.visibleImage.id = '1' + self.visibleImage.properties = image_properties image_dict = {'name': 'invisibleImage', 'container_format': 'aki'} @@ -77,23 +85,6 @@ def test_index(self): api.snapshot_list_detailed(IsA(http.HttpRequest)).AndReturn( self.snapshots) - self.mox.StubOutWithMock(api, 'flavor_list') - api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors) - api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors) - - self.mox.StubOutWithMock(api, 'security_group_list') - api.security_group_list(IsA(http.HttpRequest)).\ - AndReturn(self.security_groups) - api.security_group_list(IsA(http.HttpRequest)).\ - AndReturn(self.security_groups) - - self.mox.StubOutWithMock(api, 'keypair_list') - api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) - - self.mox.StubOutWithMock(api, 'tenant_quota_get') - api.tenant_quota_get(IsA(http.HttpRequest), - self.TEST_TENANT).AndReturn({}) - self.mox.ReplayAll() res = self.client.get(INDEX_URL) @@ -101,35 +92,18 @@ def test_index(self): self.assertTemplateUsed(res, 'nova/images_and_snapshots/index.html') - self.assertIn('images', res.context) - images = res.context['images'] + self.assertIn('images_table', res.context) + images = res.context['images_table'].data self.assertEqual(len(images), 1) self.assertEqual(images[0].name, 'visibleImage') def test_index_no_images(self): self.mox.StubOutWithMock(api, 'snapshot_list_detailed') - api.snapshot_list_detailed(IsA(http.HttpRequest)).\ - AndReturn(self.snapshots) - - self.mox.StubOutWithMock(api, 'flavor_list') - api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors) - - self.mox.StubOutWithMock(api, 'security_group_list') - api.security_group_list(IsA(http.HttpRequest)).\ - AndReturn(self.security_groups) - - self.mox.StubOutWithMock(api, 'keypair_list') - api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs) - self.mox.StubOutWithMock(api, 'image_list_detailed') - api.image_list_detailed(IsA(http.HttpRequest)).AndReturn([]) - - self.mox.StubOutWithMock(api, 'tenant_quota_get') - api.tenant_quota_get(IsA(http.HttpRequest), self.TEST_TENANT) \ - .AndReturn({}) - self.mox.StubOutWithMock(messages, 'info') - messages.info(IsA(http.HttpRequest), IsA(basestring)) + api.image_list_detailed(IsA(http.HttpRequest)).AndReturn([]) + api.snapshot_list_detailed(IsA(http.HttpRequest)).\ + AndReturn(self.snapshots) self.mox.ReplayAll() @@ -140,15 +114,12 @@ def test_index_no_images(self): def test_index_client_conn_error(self): self.mox.StubOutWithMock(api, 'image_list_detailed') + self.mox.StubOutWithMock(api, 'snapshot_list_detailed') + exception = glance_exception.ClientConnectionError('clientConnError') api.image_list_detailed(IsA(http.HttpRequest)).AndRaise(exception) - - self.mox.StubOutWithMock(api, 'tenant_quota_get') - api.tenant_quota_get(IsA(http.HttpRequest), self.TEST_TENANT) \ - .AndReturn({}) - - self.mox.StubOutWithMock(messages, 'error') - messages.error(IsA(http.HttpRequest), IsA(basestring)) + api.snapshot_list_detailed(IsA(http.HttpRequest)).\ + AndReturn(self.snapshots) self.mox.ReplayAll() diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/urls.py b/horizon/horizon/dashboards/nova/images_and_snapshots/urls.py index 6e2c6cf51c0..5fe62556cb9 100644 --- a/horizon/horizon/dashboards/nova/images_and_snapshots/urls.py +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/urls.py @@ -20,15 +20,13 @@ from django.conf.urls.defaults import * -import horizon +from .images import urls as image_urls +from .snapshots import urls as snapshot_urls +from .views import IndexView -from horizon.dashboards.nova.images_and_snapshots.images import urls\ - as image_urls -from horizon.dashboards.nova.images_and_snapshots.snapshots import urls\ - as snapshot_urls -urlpatterns = patterns('horizon.dashboards.nova.images_and_snapshots', - url(r'^$', 'views.index', name='index'), +urlpatterns = patterns('', + url(r'^$', IndexView.as_view(), name='index'), url(r'', include(image_urls, namespace='images')), url(r'', include(snapshot_urls, namespace='snapshots')), ) diff --git a/horizon/horizon/dashboards/nova/images_and_snapshots/views.py b/horizon/horizon/dashboards/nova/images_and_snapshots/views.py index befa552a943..bcbb39772fd 100644 --- a/horizon/horizon/dashboards/nova/images_and_snapshots/views.py +++ b/horizon/horizon/dashboards/nova/images_and_snapshots/views.py @@ -33,43 +33,34 @@ from openstackx.api import exceptions as api_exceptions from horizon import api -from horizon.dashboards.nova.images_and_snapshots.images.forms import \ - (UpdateImageForm, LaunchForm, DeleteImage) +from horizon import exceptions +from horizon import tables +from .images.tables import ImagesTable +from .snapshots.tables import SnapshotsTable LOG = logging.getLogger(__name__) -def index(request): - for f in (DeleteImage, ): - unused, handled = f.maybe_handle(request) - if handled: - return handled - all_images = [] - snapshots = [] - try: - all_images = api.image_list_detailed(request) - snapshots = api.snapshot_list_detailed(request) - if not all_images: - messages.info(request, _("There are currently no images.")) - except glance_exception.ClientConnectionError, e: - LOG.exception("Error connecting to glance") - messages.error(request, _("Error connecting to glance: %s") % str(e)) - except glance_exception.Error, e: - LOG.exception("Error retrieving image list") - messages.error(request, _("Unable to fetch images: %s") % str(e)) - except api_exceptions.ApiException, e: - msg = _("Unable to retrieve image info from glance: %s") % str(e) - LOG.exception(msg) - messages.error(request, msg) - images = [im for im in all_images - if im['container_format'] not in ['aki', 'ari']] +class IndexView(tables.MultiTableView): + table_classes = (ImagesTable, SnapshotsTable) + template_name = 'nova/images_and_snapshots/index.html' - quotas = api.tenant_quota_get(request, request.user.tenant_id) + def get_images_data(self): + try: + all_images = api.image_list_detailed(self.request) + images = [im for im in all_images + if im['container_format'] not in ['aki', 'ari'] and + getattr(im.properties, "image_type", '') != "snapshot"] + except: + images = [] + exceptions.handle(self.request, _("Unable to retrieve images.")) + return images - return shortcuts.render(request, - 'nova/images_and_snapshots/index.html', { - 'delete_form': DeleteImage(), - 'quotas': quotas, - 'images': images, - 'snapshots': snapshots}) + def get_snapshots_data(self): + try: + snapshots = api.snapshot_list_detailed(self.request) + except: + snapshots = [] + exceptions.handle(self.request, _("Unable to retrieve snapshots.")) + return snapshots diff --git a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_delete.html b/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_delete.html deleted file mode 100644 index 444ab2e873d..00000000000 --- a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_delete.html +++ /dev/null @@ -1,9 +0,0 @@ -{% load i18n %} -
- {% csrf_token %} - {% for hidden in form.hidden_fields %} - {{ hidden }} - {% endfor %} - - -
diff --git a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_launch.html b/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_launch.html index 859ca21baf0..b0a1a1d3949 100644 --- a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_launch.html +++ b/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_launch.html @@ -1,12 +1,11 @@ {% extends "horizon/common/_modal_form.html" %} {% load i18n %} -{% block form_id %}{% endblock %} -{% block form_action %}{% url horizon:nova:images_and_snapshots:images:launch image_id %}{% endblock %} +{% block form_id %}launch_image_form{% endblock %} +{% block form_action %}{% url horizon:nova:images_and_snapshots:images:launch image.id %}{% endblock %} -{% block modal_id %}launch_image_{{ image_id }}{% endblock %} -{% block modal_class %}launch_image modal hide{% endblock %} -{% block modal-header %}Launch Instances{% endblock %} +{% block modal_id %}launch_image_{{ image.id }}{% endblock %} +{% block modal-header %}{% trans "Launch Instances" %}{% endblock %} {% block modal-body %}
diff --git a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_list.html b/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_list.html deleted file mode 100644 index 737f5915ce1..00000000000 --- a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_list.html +++ /dev/null @@ -1,52 +0,0 @@ -{% load i18n parse_date launch_form %} - -
-

Images

-
- - {% trans "inspect" %} -
-
- - - - - - - - - - - - {% for image in images %} - - - - - - - - - {% endfor %} -
{% trans "Name" %}{% trans "Type" %}{% trans "Status" %}{% trans "Actions" %}
- - {{ image.name }}{{ image.properties.image_type|default:"Image" }}{{ image.status|capfirst }} - {% trans "Launch" %} - - {% if image.owner == request.user.tenant_id %} - More -
    -
  • {% trans "Edit" %}
  • -
  • {% include "nova/images_and_snapshots/images/_delete.html" with form=delete_form %}
  • -
- {% endif %} -
- -{% for image in images %} - {% launch_form request request.user.tenant_id image.id as launch_form %} - {% include 'nova/images_and_snapshots/images/_launch.html' with form=launch_form image_id=image.id hide=True %} -{% endfor %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_update.html b/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_update.html index 0724e69d0b5..524822d366c 100644 --- a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_update.html +++ b/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_update.html @@ -20,5 +20,5 @@

{% trans "Description:" %}

{% block modal-footer %} - {% trans "Cancel" %} + {% trans "Cancel" %} {% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/update.html b/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/update.html index cf195190ef2..da575b2142a 100644 --- a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/update.html +++ b/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/update.html @@ -1,6 +1,6 @@ {% extends 'nova/base.html' %} {% load i18n %} -{% block title %}Update Image{% endblock %} +{% block title %}{% trans "Update Image" %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Update Image") %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/index.html b/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/index.html index 576ea1eccb0..4473f15fbfe 100644 --- a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/index.html +++ b/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/index.html @@ -1,28 +1,16 @@ {% extends 'nova/base.html' %} {% load i18n %} -{% block title %}Images & Snapshots{% endblock %} +{% block title %}{% trans "Images & Snapshots" %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Images & Snapshots") %} {% endblock page_header %} {% block dash_main %} - {% if images %} - {% include 'nova/images_and_snapshots/images/_list.html' %} - {% else %} -
-

{% trans "Info: " %}{% trans "There are currently no images." %}

-
- {% endif %} - - {% if snapshots %} - {% include 'nova/images_and_snapshots/snapshots/_list.html' %} - {% else %} -
-

{% trans "Info: " %}{% trans "There are currently no snapshots. You can create snapshots from running instances." %}

- -
- {% endif %} +
+ {{ images_table.render }} +
+
+ {{ snapshots_table.render }} +
{% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/snapshots/_create.html b/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/snapshots/_create.html index 3bd3bc11ae6..41a531666f6 100644 --- a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/snapshots/_create.html +++ b/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/snapshots/_create.html @@ -21,5 +21,5 @@

{% trans "Description:" %}

{% block modal-footer %} - {% trans "Cancel" %} + {% trans "Cancel" %} {% endblock %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/snapshots/_list.html b/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/snapshots/_list.html deleted file mode 100644 index 55ecbacec87..00000000000 --- a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/snapshots/_list.html +++ /dev/null @@ -1,53 +0,0 @@ -{% load i18n parse_date launch_form %} - - -
-

Snapshots

-
- - {% trans "inspect" %} -
-
- - - - - - - - - - - - {% for image in snapshots %} - - - - - - - - - {% endfor %} -
{% trans "Name" %}{% trans "Type" %}{% trans "Status" %}{% trans "Actions" %}
- - {{ image.name }}{{ image.properties.image_type|default:"Image" }}{{ image.status|capfirst }} - {% trans "Launch" %} - - {% if image.owner == request.user.tenant_id %} - View -
    -
  • {% trans "Edit" %}
  • -
  • {% include "nova/images_and_snapshots/images/_delete.html" with form=delete_form %}
  • -
- {% endif %} -
- -{% for image in snapshots %} - {% launch_form request request.user.tenant_id image.id as launch_form %} - {% include 'nova/images_and_snapshots/images/_launch.html' with form=launch_form image_id=image.id hide=True %} -{% endfor %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/snapshots/index.html b/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/snapshots/index.html deleted file mode 100644 index eac5c71802c..00000000000 --- a/horizon/horizon/dashboards/nova/templates/nova/images_and_snapshots/snapshots/index.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends 'nova/base.html' %} -{% load i18n %} -{% block title %}Snapshots{% endblock %} - -{% block page_header %} - {% url horizon:nova:snapshots:index as refresh_link %} - {# to make searchable false, just remove it from the include statement #} - {% include "horizon/common/_page_header.html" with title=_("Snapshots") refresh_link=refresh_link searchable="true" %} -{% endblock page_header %} - -{% block dash_main %} - {% if images %} - {% include 'nova/images_and_snapshots/images/_list.html' %} - {% else %} -
-

{% trans "Info: " %}{% trans "There are currently no snapshots. You can create snapshots from running instances." %}

- -
- {% endif %} -{% endblock %} - diff --git a/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/instances/_no_instances.html b/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/instances/_no_instances.html index fd40d51a3d8..7c768d65f26 100644 --- a/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/instances/_no_instances.html +++ b/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/instances/_no_instances.html @@ -3,6 +3,6 @@

{% trans "Info: " %}{% trans "There are no instances running. Launch an instance from the Images Page." %}

diff --git a/horizon/horizon/exceptions.py b/horizon/horizon/exceptions.py index 6299d0fc537..800e1094485 100644 --- a/horizon/horizon/exceptions.py +++ b/horizon/horizon/exceptions.py @@ -22,6 +22,7 @@ import sys from django.contrib import messages +from glance.common import exception as glanceclient from keystoneclient import exceptions as keystoneclient from novaclient import exceptions as novaclient from openstackx.api import exceptions as openstackx @@ -35,16 +36,20 @@ keystoneclient.Unauthorized, keystoneclient.Forbidden, novaclient.Unauthorized, - novaclient.Forbidden) + novaclient.Forbidden, + glanceclient.AuthorizationFailure, + glanceclient.NotAuthorized) NOT_FOUND = (keystoneclient.NotFound, novaclient.NotFound, - openstackx.NotFound) + openstackx.NotFound, + glanceclient.NotFound) # NOTE(gabriel): This is very broad, and may need to be dialed in. RECOVERABLE = (keystoneclient.ClientException, novaclient.ClientException, - openstackx.ApiException) + openstackx.ApiException, + glanceclient.GlanceException) class Http302(Exception): @@ -52,8 +57,9 @@ class Http302(Exception): Error class which can be raised from within a handler to cause an early bailout and redirect at the middleware level. """ - def __init__(self, location): + def __init__(self, location, message=None): self.location = location + self.message = message class NotAuthorized(Exception): @@ -125,6 +131,10 @@ def handle(request, message=None, redirect=None, ignore=False, escalate=False): exc_type, exc_value, exc_traceback = exc_value.wrapped wrap = True + # If the message has a placeholder for the exception, fill it in + if message and "%(exc)s" in message: + message = message % {"exc": exc_value} + if issubclass(exc_type, UNAUTHORIZED): if ignore: return diff --git a/horizon/horizon/middleware.py b/horizon/horizon/middleware.py index 1a171866fc2..01559e943ec 100644 --- a/horizon/horizon/middleware.py +++ b/horizon/horizon/middleware.py @@ -68,4 +68,6 @@ def process_exception(self, request, exception): return shortcuts.redirect('/auth/login') if isinstance(exception, exceptions.Http302): + if exception.message: + messages.error(request, exception.message) return shortcuts.redirect(exception.location) diff --git a/horizon/horizon/tables/base.py b/horizon/horizon/tables/base.py index 821dec117dd..0a419e89e66 100644 --- a/horizon/horizon/tables/base.py +++ b/horizon/horizon/tables/base.py @@ -31,6 +31,7 @@ from django.utils.translation import ugettext as _ from django.utils.safestring import mark_safe +from horizon import exceptions from .actions import FilterAction, LinkAction @@ -687,7 +688,9 @@ def get_object_by_id(self, lookup): raise ValueError("Multiple matches were returned for that id: %s." % matches) if not matches: - raise ValueError('No match returned for the id "%s".' % lookup) + raise exceptions.Http302(self.get_absolute_url(), + _('No match returned for the id "%s".') + % lookup) return matches[0] def get_table_actions(self): diff --git a/horizon/horizon/templatetags/launch_form.py b/horizon/horizon/templatetags/launch_form.py deleted file mode 100644 index ddcfa70734c..00000000000 --- a/horizon/horizon/templatetags/launch_form.py +++ /dev/null @@ -1,78 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2011 Openstack LLC -# -# 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. - -""" -Template tags for dynamic creation of image launch form. -""" - -from __future__ import absolute_import -import logging -from django import template -from openstackx.api import exceptions as api_exceptions -from novaclient import exceptions as novaclient_exceptions -from horizon import api -from horizon.dashboards.nova.images_and_snapshots.images.forms import \ - LaunchForm - -from horizon.utils import assignment_tag - -LOG = logging.getLogger(__name__) -register = template.Library() - - -@register.assignment_tag -def launch_form(request, tenant_id, image_id): - - def flavorlist(request): - try: - fl = api.flavor_list(request) - - # TODO add vcpu count to flavors - sel = [(f.id, '%s (%svcpu / %sGB Disk / %sMB Ram )' % - (f.name, f.vcpus, f.disk, f.ram)) for f in fl] - return sorted(sel) - except api_exceptions.ApiException: - LOG.exception('Unable to retrieve list of instance types') - return [(1, 'm1.tiny')] - - def keynamelist(request): - try: - fl = api.keypair_list(request) - sel = [(f.name, f.name) for f in fl] - return sel - except Exception: - LOG.exception('Unable to retrieve list of keypairs') - return [] - - def securitygrouplist(request): - try: - fl = api.security_group_list(request) - sel = [(f.name, f.name) for f in fl] - return sel - except novaclient_exceptions.ClientException: - LOG.exception('Unable to retrieve list of security groups') - return [] - - form = LaunchForm(initial={'flavorlist': flavorlist(request), - 'keynamelist': keynamelist(request), - 'securitygrouplist': securitygrouplist(request), - 'image_id': image_id, - 'tenant_id': tenant_id}) - return form diff --git a/horizon/horizon/tests/table_tests.py b/horizon/horizon/tests/table_tests.py index 2eca022bd16..a241b953c78 100644 --- a/horizon/horizon/tests/table_tests.py +++ b/horizon/horizon/tests/table_tests.py @@ -297,7 +297,6 @@ def test_table_column(self): row = self.table.get_rows()[0] self.assertTrue("down" in row.cells['status'].value) - def test_table_row(self): self.table = MyTable(self.request, TEST_DATA) row = self.table.get_rows()[0]