Merge "Move volume snapshots table to volumes panel"

This commit is contained in:
Jenkins 2014-02-24 04:33:32 +00:00 committed by Gerrit Code Review
commit c03e9ff05e
73 changed files with 710 additions and 608 deletions

View File

@ -18,8 +18,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from openstack_dashboard.dashboards.project.images_and_snapshots \ from openstack_dashboard.dashboards.project.images.images import forms
.images import forms
class AdminCreateImageForm(forms.CreateImageForm): class AdminCreateImageForm(forms.CreateImageForm):

View File

@ -19,7 +19,7 @@ from django.utils.translation import ugettext_lazy as _
from horizon import tables from horizon import tables
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.dashboards.project.images_and_snapshots.images \ from openstack_dashboard.dashboards.project.images.images \
import tables as project_tables import tables as project_tables

View File

@ -25,8 +25,7 @@ from horizon import exceptions
from horizon import tables from horizon import tables
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.dashboards.project \ from openstack_dashboard.dashboards.project.images.images import views
.images_and_snapshots.images import views
from openstack_dashboard.dashboards.admin.images import forms from openstack_dashboard.dashboards.admin.images import forms
from openstack_dashboard.dashboards.admin.images \ from openstack_dashboard.dashboards.admin.images \

View File

@ -15,7 +15,7 @@ from django.utils.translation import ugettext_lazy as _
from horizon import tables from horizon import tables
from openstack_dashboard.api import cinder from openstack_dashboard.api import cinder
from openstack_dashboard.dashboards.project.volumes \ from openstack_dashboard.dashboards.project.volumes \
import tables as project_tables .volumes import tables as project_tables
class CreateVolumeType(tables.LinkAction): class CreateVolumeType(tables.LinkAction):

View File

@ -34,10 +34,13 @@ from openstack_dashboard.dashboards.admin.volumes \
from openstack_dashboard.dashboards.admin.volumes \ from openstack_dashboard.dashboards.admin.volumes \
import tables as project_tables import tables as project_tables
from openstack_dashboard.dashboards.project.volumes import views from openstack_dashboard.dashboards.project.volumes \
import tabs as project_tabs
from openstack_dashboard.dashboards.project.volumes \
.volumes import views as volume_views
class IndexView(tables.MultiTableView, views.VolumeTableMixIn): class IndexView(tables.MultiTableView, project_tabs.VolumeTableMixIn):
table_classes = (project_tables.VolumesTable, table_classes = (project_tables.VolumesTable,
project_tables.VolumeTypesTable) project_tables.VolumeTypesTable)
template_name = "admin/volumes/index.html" template_name = "admin/volumes/index.html"
@ -74,7 +77,7 @@ class IndexView(tables.MultiTableView, views.VolumeTableMixIn):
return volume_types return volume_types
class DetailView(views.DetailView): class DetailView(volume_views.DetailView):
template_name = "admin/volumes/detail.html" template_name = "admin/volumes/detail.html"

View File

@ -25,7 +25,7 @@ class BasePanels(horizon.PanelGroup):
panels = ('overview', panels = ('overview',
'instances', 'instances',
'volumes', 'volumes',
'images_and_snapshots', 'images',
'access_and_security',) 'access_and_security',)

View File

@ -77,7 +77,7 @@ class DeleteImage(tables.DeleteAction):
class CreateImage(tables.LinkAction): class CreateImage(tables.LinkAction):
name = "create" name = "create"
verbose_name = _("Create Image") verbose_name = _("Create Image")
url = "horizon:project:images_and_snapshots:images:create" url = "horizon:project:images:images:create"
classes = ("ajax-modal", "btn-create") classes = ("ajax-modal", "btn-create")
policy_rules = (("image", "add_image"),) policy_rules = (("image", "add_image"),)
@ -85,7 +85,7 @@ class CreateImage(tables.LinkAction):
class EditImage(tables.LinkAction): class EditImage(tables.LinkAction):
name = "edit" name = "edit"
verbose_name = _("Edit") verbose_name = _("Edit")
url = "horizon:project:images_and_snapshots:images:update" url = "horizon:project:images:images:update"
classes = ("ajax-modal", "btn-edit") classes = ("ajax-modal", "btn-edit")
policy_rules = (("image", "modify_image"),) policy_rules = (("image", "modify_image"),)
@ -101,7 +101,7 @@ class EditImage(tables.LinkAction):
class CreateVolumeFromImage(tables.LinkAction): class CreateVolumeFromImage(tables.LinkAction):
name = "create_volume_from_image" name = "create_volume_from_image"
verbose_name = _("Create Volume") verbose_name = _("Create Volume")
url = "horizon:project:volumes:create" url = "horizon:project:volumes:volumes:create"
classes = ("ajax-modal", "btn-camera") classes = ("ajax-modal", "btn-camera")
policy_rules = (("volume", "volume:create"),) policy_rules = (("volume", "volume:create"),)
@ -206,8 +206,7 @@ class ImagesTable(tables.DataTable):
("deleted", False), ("deleted", False),
) )
name = tables.Column(get_image_name, name = tables.Column(get_image_name,
link=("horizon:project:images_and_snapshots:" link=("horizon:project:images:images:detail"),
"images:detail"),
verbose_name=_("Image Name")) verbose_name=_("Image Name"))
image_type = tables.Column(get_image_type, image_type = tables.Column(get_image_type,
verbose_name=_("Type"), verbose_name=_("Type"),

View File

@ -23,7 +23,7 @@ from horizon import tabs
class OverviewTab(tabs.Tab): class OverviewTab(tabs.Tab):
name = _("Overview") name = _("Overview")
slug = "overview" slug = "overview"
template_name = "project/images_and_snapshots/images/_detail_overview.html" template_name = "project/images/images/_detail_overview.html"
def get_context_data(self, request): def get_context_data(self, request):
image = self.tab_group.kwargs['image'] image = self.tab_group.kwargs['image']

View File

@ -33,13 +33,11 @@ from horizon import tables as horizon_tables
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
from openstack_dashboard.dashboards.project.images_and_snapshots.images \ from openstack_dashboard.dashboards.project.images.images import forms
import forms from openstack_dashboard.dashboards.project.images.images import tables
from openstack_dashboard.dashboards.project.images_and_snapshots.images \
import tables
IMAGES_INDEX_URL = reverse('horizon:project:images_and_snapshots:index') IMAGES_INDEX_URL = reverse('horizon:project:images:index')
class CreateImageFormTests(test.TestCase): class CreateImageFormTests(test.TestCase):
@ -74,10 +72,10 @@ class CreateImageFormTests(test.TestCase):
class ImageViewTests(test.TestCase): class ImageViewTests(test.TestCase):
def test_image_create_get(self): def test_image_create_get(self):
url = reverse('horizon:project:images_and_snapshots:images:create') url = reverse('horizon:project:images:images:create')
res = self.client.get(url) res = self.client.get(url)
self.assertTemplateUsed(res, self.assertTemplateUsed(res,
'project/images_and_snapshots/images/create.html') 'project/images/images/create.html')
@test.create_stubs({api.glance: ('image_create',)}) @test.create_stubs({api.glance: ('image_create',)})
def test_image_create_post_copy_from(self): def test_image_create_post_copy_from(self):
@ -111,7 +109,7 @@ class ImageViewTests(test.TestCase):
AndReturn(self.images.first()) AndReturn(self.images.first())
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:images_and_snapshots:images:create') url = reverse('horizon:project:images:images:create')
res = self.client.post(url, data) res = self.client.post(url, data)
self.assertNoFormErrors(res) self.assertNoFormErrors(res)
@ -151,7 +149,7 @@ class ImageViewTests(test.TestCase):
AndReturn(self.images.first()) AndReturn(self.images.first())
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:images_and_snapshots:images:create') url = reverse('horizon:project:images:images:create')
res = self.client.post(url, data) res = self.client.post(url, data)
self.assertNoFormErrors(res) self.assertNoFormErrors(res)
@ -165,12 +163,11 @@ class ImageViewTests(test.TestCase):
.AndReturn(self.images.first()) .AndReturn(self.images.first())
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get( res = self.client.get(reverse('horizon:project:images:images:detail',
reverse('horizon:project:images_and_snapshots:images:detail', args=[image.id]))
args=[image.id]))
self.assertTemplateUsed(res, self.assertTemplateUsed(res,
'project/images_and_snapshots/images/detail.html') 'project/images/images/detail.html')
self.assertEqual(res.context['image'].name, image.name) self.assertEqual(res.context['image'].name, image.name)
self.assertEqual(res.context['image'].protected, image.protected) self.assertEqual(res.context['image'].protected, image.protected)
self.assertContains(res, "<h2>Image Details: %s</h2>" % image.name, self.assertContains(res, "<h2>Image Details: %s</h2>" % image.name,
@ -184,9 +181,8 @@ class ImageViewTests(test.TestCase):
.AndReturn(image) .AndReturn(image)
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get( res = self.client.get(reverse('horizon:project:images:images:detail',
reverse('horizon:project:images_and_snapshots:images:detail', args=[image.id]))
args=[image.id]))
image_props = res.context['image_props'] image_props = res.context['image_props']
@ -213,10 +209,10 @@ class ImageViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get( res = self.client.get(
reverse('horizon:project:images_and_snapshots:images:detail', reverse('horizon:project:images:images:detail',
args=[image.id])) args=[image.id]))
self.assertTemplateUsed(res, self.assertTemplateUsed(res,
'project/images_and_snapshots/images/detail.html') 'project/images/images/detail.html')
self.assertEqual(res.context['image'].protected, image.protected) self.assertEqual(res.context['image'].protected, image.protected)
@test.create_stubs({api.glance: ('image_get',)}) @test.create_stubs({api.glance: ('image_get',)})
@ -227,7 +223,7 @@ class ImageViewTests(test.TestCase):
.AndRaise(self.exceptions.glance) .AndRaise(self.exceptions.glance)
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:images_and_snapshots:images:detail', url = reverse('horizon:project:images:images:detail',
args=[image.id]) args=[image.id])
res = self.client.get(url) res = self.client.get(url)
self.assertRedirectsNoFollow(res, IMAGES_INDEX_URL) self.assertRedirectsNoFollow(res, IMAGES_INDEX_URL)
@ -242,11 +238,11 @@ class ImageViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get( res = self.client.get(
reverse('horizon:project:images_and_snapshots:images:update', reverse('horizon:project:images:images:update',
args=[image.id])) args=[image.id]))
self.assertTemplateUsed(res, self.assertTemplateUsed(res,
'project/images_and_snapshots/images/_update.html') 'project/images/images/_update.html')
self.assertEqual(res.context['image'].name, image.name) self.assertEqual(res.context['image'].name, image.name)
# Bug 1076216 - is_public checkbox not being set correctly # Bug 1076216 - is_public checkbox not being set correctly
self.assertContains(res, "<input type='checkbox' id='id_public'" self.assertContains(res, "<input type='checkbox' id='id_public'"

View File

@ -21,12 +21,10 @@
from django.conf.urls import patterns # noqa from django.conf.urls import patterns # noqa
from django.conf.urls import url # noqa from django.conf.urls import url # noqa
from openstack_dashboard.dashboards.project.images_and_snapshots.images \ from openstack_dashboard.dashboards.project.images.images import views
import views
VIEWS_MOD = ('openstack_dashboard.dashboards.project' VIEWS_MOD = 'openstack_dashboard.dashboards.project.images.images.views'
'.images_and_snapshots.images.views')
urlpatterns = patterns(VIEWS_MOD, urlpatterns = patterns(VIEWS_MOD,

View File

@ -32,23 +32,23 @@ from horizon.utils import memoized
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.dashboards.project.images_and_snapshots.images \ from openstack_dashboard.dashboards.project.images.images \
import forms as project_forms import forms as project_forms
from openstack_dashboard.dashboards.project.images_and_snapshots.images \ from openstack_dashboard.dashboards.project.images.images \
import tabs as project_tabs import tabs as project_tabs
class CreateView(forms.ModalFormView): class CreateView(forms.ModalFormView):
form_class = project_forms.CreateImageForm form_class = project_forms.CreateImageForm
template_name = 'project/images_and_snapshots/images/create.html' template_name = 'project/images/images/create.html'
context_object_name = 'image' context_object_name = 'image'
success_url = reverse_lazy("horizon:project:images_and_snapshots:index") success_url = reverse_lazy("horizon:project:images:index")
class UpdateView(forms.ModalFormView): class UpdateView(forms.ModalFormView):
form_class = project_forms.UpdateImageForm form_class = project_forms.UpdateImageForm
template_name = 'project/images_and_snapshots/images/update.html' template_name = 'project/images/images/update.html'
success_url = reverse_lazy("horizon:project:images_and_snapshots:index") success_url = reverse_lazy("horizon:project:images:index")
@memoized.memoized_method @memoized.memoized_method
def get_object(self): def get_object(self):
@ -56,7 +56,7 @@ class UpdateView(forms.ModalFormView):
return api.glance.image_get(self.request, self.kwargs['image_id']) return api.glance.image_get(self.request, self.kwargs['image_id'])
except Exception: except Exception:
msg = _('Unable to retrieve image.') msg = _('Unable to retrieve image.')
url = reverse('horizon:project:images_and_snapshots:index') url = reverse('horizon:project:images:index')
exceptions.handle(self.request, msg, redirect=url) exceptions.handle(self.request, msg, redirect=url)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -80,7 +80,7 @@ class UpdateView(forms.ModalFormView):
class DetailView(tabs.TabView): class DetailView(tabs.TabView):
tab_group_class = project_tabs.ImageDetailTabs tab_group_class = project_tabs.ImageDetailTabs
template_name = 'project/images_and_snapshots/images/detail.html' template_name = 'project/images/images/detail.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs) context = super(DetailView, self).get_context_data(**kwargs)
@ -92,7 +92,7 @@ class DetailView(tabs.TabView):
try: try:
return api.glance.image_get(self.request, self.kwargs['image_id']) return api.glance.image_get(self.request, self.kwargs['image_id'])
except Exception: except Exception:
url = reverse('horizon:project:images_and_snapshots:index') url = reverse('horizon:project:images:index')
exceptions.handle(self.request, exceptions.handle(self.request,
_('Unable to retrieve image details.'), _('Unable to retrieve image details.'),
redirect=url) redirect=url)

View File

@ -22,9 +22,9 @@ import horizon
from openstack_dashboard.dashboards.project import dashboard from openstack_dashboard.dashboards.project import dashboard
class ImagesAndSnapshots(horizon.Panel): class Images(horizon.Panel):
name = _("Images & Snapshots") name = _("Images")
slug = 'images_and_snapshots' slug = 'images'
dashboard.Project.register(ImagesAndSnapshots) dashboard.Project.register(Images)

View File

@ -27,7 +27,7 @@ from openstack_dashboard import api
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
INDEX_URL = reverse('horizon:project:images_and_snapshots:index') INDEX_URL = reverse('horizon:project:images:index')
class SnapshotsViewTests(test.TestCase): class SnapshotsViewTests(test.TestCase):
@ -37,11 +37,11 @@ class SnapshotsViewTests(test.TestCase):
api.nova.server_get(IsA(http.HttpRequest), server.id).AndReturn(server) api.nova.server_get(IsA(http.HttpRequest), server.id).AndReturn(server)
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:images_and_snapshots:snapshots:create', url = reverse('horizon:project:images:snapshots:create',
args=[server.id]) args=[server.id])
res = self.client.get(url) res = self.client.get(url)
self.assertTemplateUsed(res, self.assertTemplateUsed(res,
'project/images_and_snapshots/snapshots/create.html') 'project/images/snapshots/create.html')
def test_create_get_server_exception(self): def test_create_get_server_exception(self):
server = self.servers.first() server = self.servers.first()
@ -50,7 +50,7 @@ class SnapshotsViewTests(test.TestCase):
.AndRaise(self.exceptions.nova) .AndRaise(self.exceptions.nova)
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:images_and_snapshots:snapshots:create', url = reverse('horizon:project:images:snapshots:create',
args=[server.id]) args=[server.id])
res = self.client.get(url) res = self.client.get(url)
redirect = reverse("horizon:project:instances:index") redirect = reverse("horizon:project:instances:index")
@ -71,7 +71,7 @@ class SnapshotsViewTests(test.TestCase):
'tenant_id': self.tenant.id, 'tenant_id': self.tenant.id,
'instance_id': server.id, 'instance_id': server.id,
'name': snapshot.name} 'name': snapshot.name}
url = reverse('horizon:project:images_and_snapshots:snapshots:create', url = reverse('horizon:project:images:snapshots:create',
args=[server.id]) args=[server.id])
res = self.client.post(url, formData) res = self.client.post(url, formData)
@ -91,7 +91,7 @@ class SnapshotsViewTests(test.TestCase):
'tenant_id': self.tenant.id, 'tenant_id': self.tenant.id,
'instance_id': server.id, 'instance_id': server.id,
'name': snapshot.name} 'name': snapshot.name}
url = reverse('horizon:project:images_and_snapshots:snapshots:create', url = reverse('horizon:project:images:snapshots:create',
args=[server.id]) args=[server.id])
res = self.client.post(url, formData) res = self.client.post(url, formData)
redirect = reverse("horizon:project:instances:index") redirect = reverse("horizon:project:instances:index")

View File

@ -21,8 +21,7 @@
from django.conf.urls import patterns # noqa from django.conf.urls import patterns # noqa
from django.conf.urls import url # noqa from django.conf.urls import url # noqa
from openstack_dashboard.dashboards.project.images_and_snapshots.snapshots \ from openstack_dashboard.dashboards.project.images.snapshots import views
import views
urlpatterns = patterns('', urlpatterns = patterns('',

View File

@ -31,14 +31,14 @@ from horizon.utils import memoized
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.dashboards.project.images_and_snapshots.snapshots \ from openstack_dashboard.dashboards.project.images.snapshots \
import forms as project_forms import forms as project_forms
class CreateView(forms.ModalFormView): class CreateView(forms.ModalFormView):
form_class = project_forms.CreateSnapshot form_class = project_forms.CreateSnapshot
template_name = 'project/images_and_snapshots/snapshots/create.html' template_name = 'project/images/snapshots/create.html'
success_url = reverse_lazy("horizon:project:images_and_snapshots:index") success_url = reverse_lazy("horizon:project:images:index")
@memoized.memoized_method @memoized.memoized_method
def get_object(self): def get_object(self):

View File

@ -3,7 +3,7 @@
{% load url from future %} {% load url from future %}
{% block form_id %}create_image_form{% endblock %} {% block form_id %}create_image_form{% endblock %}
{% block form_action %}{% url 'horizon:project:images_and_snapshots:images:create' %}{% endblock %} {% block form_action %}{% url 'horizon:project:images:images:create' %}{% endblock %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %} {% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal-header %}{% trans "Create An Image" %}{% endblock %} {% block modal-header %}{% trans "Create An Image" %}{% endblock %}
@ -31,5 +31,5 @@
{% block modal-footer %} {% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Image" %}" /> <input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Image" %}" />
<a href="{% url 'horizon:project:images_and_snapshots:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a> <a href="{% url 'horizon:project:images:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %} {% endblock %}

View File

@ -3,7 +3,7 @@
{% load url from future %} {% load url from future %}
{% block form_id %}update_image_form{% endblock %} {% block form_id %}update_image_form{% endblock %}
{% block form_action %}{% url 'horizon:project:images_and_snapshots:images:update' image.id %}{% endblock %} {% block form_action %}{% url 'horizon:project:images:images:update' image.id %}{% endblock %}
{% block modal-header %}{% trans "Update Image" %}{% endblock %} {% block modal-header %}{% trans "Update Image" %}{% endblock %}
@ -21,5 +21,5 @@
{% block modal-footer %} {% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Update Image" %}" /> <input class="btn btn-primary pull-right" type="submit" value="{% trans "Update Image" %}" />
<a href="{% url 'horizon:project:images_and_snapshots:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a> <a href="{% url 'horizon:project:images:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %} {% endblock %}

View File

@ -7,5 +7,5 @@
{% endblock page_header %} {% endblock page_header %}
{% block main %} {% block main %}
{% include 'project/images_and_snapshots/images/_create.html' %} {% include 'project/images/images/_create.html' %}
{% endblock %} {% endblock %}

View File

@ -7,5 +7,5 @@
{% endblock page_header %} {% endblock page_header %}
{% block main %} {% block main %}
{% include 'project/images_and_snapshots/images/_update.html' %} {% include 'project/images/images/_update.html' %}
{% endblock %} {% endblock %}

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Images" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Images") %}
{% endblock page_header %}
{% block main %}
{{ table.render }}
{% endblock %}

View File

@ -3,7 +3,7 @@
{% load url from future %} {% load url from future %}
{% block form_id %}create_snapshot_form{% endblock %} {% block form_id %}create_snapshot_form{% endblock %}
{% block form_action %}{% url 'horizon:project:images_and_snapshots:snapshots:create' instance.id %}{% endblock %} {% block form_action %}{% url 'horizon:project:images:snapshots:create' instance.id %}{% endblock %}
{% block modal_id %}create_snapshot_modal{% endblock %} {% block modal_id %}create_snapshot_modal{% endblock %}
{% block modal-header %}{% trans "Create Snapshot" %}{% endblock %} {% block modal-header %}{% trans "Create Snapshot" %}{% endblock %}
@ -22,5 +22,5 @@
{% block modal-footer %} {% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Snapshot" %}" /> <input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Snapshot" %}" />
<a href="{% url 'horizon:project:images_and_snapshots:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a> <a href="{% url 'horizon:project:images:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %} {% endblock %}

View File

@ -7,5 +7,5 @@
{% endblock page_header %} {% endblock page_header %}
{% block main %} {% block main %}
{% include 'project/images_and_snapshots/snapshots/_create.html' %} {% include 'project/images/snapshots/_create.html' %}
{% endblock %} {% endblock %}

View File

@ -27,32 +27,23 @@ from mox import IsA # noqa
from horizon import exceptions from horizon import exceptions
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.dashboards.project.images_and_snapshots import utils from openstack_dashboard.dashboards.project.images import utils
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
INDEX_URL = reverse('horizon:project:images_and_snapshots:index') INDEX_URL = reverse('horizon:project:images:index')
class ImagesAndSnapshotsTests(test.TestCase): class ImagesAndSnapshotsTests(test.TestCase):
@test.create_stubs({api.glance: ('image_list_detailed',), @test.create_stubs({api.glance: ('image_list_detailed',)})
api.cinder: ('volume_snapshot_list',
'volume_list',)})
def test_index(self): def test_index(self):
images = self.images.list() images = self.images.list()
vol_snaps = self.volume_snapshots.list()
volumes = self.volumes.list()
api.cinder.volume_snapshot_list(IsA(http.HttpRequest)) \
.AndReturn(vol_snaps)
api.cinder.volume_list(IsA(http.HttpRequest)) \
.AndReturn(volumes)
api.glance.image_list_detailed(IsA(http.HttpRequest), api.glance.image_list_detailed(IsA(http.HttpRequest),
marker=None).AndReturn([images, False]) marker=None).AndReturn([images, False])
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get(INDEX_URL) res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'project/images_and_snapshots/index.html') self.assertTemplateUsed(res, 'project/images/index.html')
self.assertIn('images_table', res.context) self.assertIn('images_table', res.context)
images_table = res.context['images_table'] images_table = res.context['images_table']
images = images_table.data images = images_table.data
@ -67,61 +58,34 @@ class ImagesAndSnapshotsTests(test.TestCase):
row_actions = images_table.get_row_actions(images[2]) row_actions = images_table.get_row_actions(images[2])
self.assertTrue(len(row_actions), 3) self.assertTrue(len(row_actions), 3)
@test.create_stubs({api.glance: ('image_list_detailed',), @test.create_stubs({api.glance: ('image_list_detailed',)})
api.cinder: ('volume_snapshot_list',
'volume_list',)})
def test_index_no_images(self): def test_index_no_images(self):
vol_snaps = self.volume_snapshots.list()
volumes = self.volumes.list()
api.cinder.volume_snapshot_list(IsA(http.HttpRequest)) \
.AndReturn(vol_snaps)
api.cinder.volume_list(IsA(http.HttpRequest)) \
.AndReturn(volumes)
api.glance.image_list_detailed(IsA(http.HttpRequest), api.glance.image_list_detailed(IsA(http.HttpRequest),
marker=None).AndReturn([(), False]) marker=None).AndReturn([(), False])
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get(INDEX_URL) res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'project/images_and_snapshots/index.html') self.assertTemplateUsed(res, 'project/images/index.html')
@test.create_stubs({api.glance: ('image_list_detailed',), @test.create_stubs({api.glance: ('image_list_detailed',)})
api.cinder: ('volume_snapshot_list',
'volume_list',)})
def test_index_error(self): def test_index_error(self):
vol_snaps = self.volume_snapshots.list()
volumes = self.volumes.list()
api.cinder.volume_snapshot_list(IsA(http.HttpRequest)) \
.AndReturn(vol_snaps)
api.cinder.volume_list(IsA(http.HttpRequest)) \
.AndReturn(volumes)
api.glance.image_list_detailed(IsA(http.HttpRequest), api.glance.image_list_detailed(IsA(http.HttpRequest),
marker=None) \ marker=None) \
.AndRaise(self.exceptions.glance) .AndRaise(self.exceptions.glance)
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get(INDEX_URL) res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'project/images_and_snapshots/index.html') self.assertTemplateUsed(res, 'project/images/index.html')
@test.create_stubs({api.glance: ('image_list_detailed',), @test.create_stubs({api.glance: ('image_list_detailed',)})
api.cinder: ('volume_snapshot_list',
'volume_list',)})
def test_snapshot_actions(self): def test_snapshot_actions(self):
snapshots = self.snapshots.list() snapshots = self.snapshots.list()
vol_snaps = self.volume_snapshots.list()
volumes = self.volumes.list()
api.cinder.volume_snapshot_list(IsA(http.HttpRequest)) \
.AndReturn(vol_snaps)
api.cinder.volume_list(IsA(http.HttpRequest)) \
.AndReturn(volumes)
api.glance.image_list_detailed(IsA(http.HttpRequest), marker=None) \ api.glance.image_list_detailed(IsA(http.HttpRequest), marker=None) \
.AndReturn([snapshots, False]) .AndReturn([snapshots, False])
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get(INDEX_URL) res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'project/images_and_snapshots/index.html') self.assertTemplateUsed(res, 'project/images/index.html')
self.assertIn('images_table', res.context) self.assertIn('images_table', res.context)
snaps = res.context['images_table'] snaps = res.context['images_table']
self.assertEqual(len(snaps.get_rows()), 3) self.assertEqual(len(snaps.get_rows()), 3)

View File

@ -22,18 +22,15 @@ from django.conf.urls import include # noqa
from django.conf.urls import patterns # noqa from django.conf.urls import patterns # noqa
from django.conf.urls import url # noqa from django.conf.urls import url # noqa
from openstack_dashboard.dashboards.project.images_and_snapshots.images \ from openstack_dashboard.dashboards.project.images.images \
import urls as image_urls import urls as image_urls
from openstack_dashboard.dashboards.project.images_and_snapshots.snapshots \ from openstack_dashboard.dashboards.project.images.snapshots \
import urls as snapshot_urls import urls as snapshot_urls
from openstack_dashboard.dashboards.project.images_and_snapshots import views from openstack_dashboard.dashboards.project.images import views
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^$', views.IndexView.as_view(), name='index'), url(r'^$', views.IndexView.as_view(), name='index'),
url(r'', include(image_urls, namespace='images')), url(r'', include(image_urls, namespace='images')),
url(r'', include(snapshot_urls, namespace='snapshots')), url(r'', include(snapshot_urls, namespace='snapshots')),
url(r'^snapshots/(?P<snapshot_id>[^/]+)/$',
views.DetailView.as_view(),
name='detail'),
) )

View File

@ -0,0 +1,54 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
# Copyright 2012 OpenStack Foundation
#
# 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.
"""
Views for managing Images and Snapshots.
"""
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.images.images \
import tables as images_tables
class IndexView(tables.DataTableView):
table_class = images_tables.ImagesTable
template_name = 'project/images/index.html'
def has_more_data(self, table):
return getattr(self, "_more_%s" % table.name, False)
def get_data(self):
marker = self.request.GET.get(
images_tables.ImagesTable._meta.pagination_param, None)
try:
(images,
self._more_images) = api.glance.image_list_detailed(self.request,
marker=marker)
except Exception:
images = []
exceptions.handle(self.request, _("Unable to retrieve images."))
return images

View File

@ -1,16 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Images &amp; Snapshots" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Images &amp; Snapshots") %}
{% endblock page_header %}
{% block main %}
<div class="images">
{{ images_table.render }}
</div>
<div class="volume_snapshots">
{{ volume_snapshots_table.render }}
</div>
{% endblock %}

View File

@ -1,108 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
# Copyright 2012 OpenStack Foundation
#
# 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.
"""
Views for managing Images and Snapshots.
"""
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tables
from horizon import tabs
from horizon.utils import memoized
from openstack_dashboard import api
from openstack_dashboard.api import base
from openstack_dashboard.dashboards.project.images_and_snapshots.images \
import tables as images_tables
from openstack_dashboard.dashboards.project.images_and_snapshots.\
volume_snapshots import tables as vol_snsh_tables
from openstack_dashboard.dashboards.project.images_and_snapshots.\
volume_snapshots import tabs as vol_snsh_tabs
class IndexView(tables.MultiTableView):
table_classes = (images_tables.ImagesTable,
vol_snsh_tables.VolumeSnapshotsTable)
template_name = 'project/images_and_snapshots/index.html'
def has_more_data(self, table):
return getattr(self, "_more_%s" % table.name, False)
def get_images_data(self):
marker = self.request.GET.get(
images_tables.ImagesTable._meta.pagination_param, None)
try:
(images,
self._more_images) = api.glance.image_list_detailed(self.request,
marker=marker)
except Exception:
images = []
exceptions.handle(self.request, _("Unable to retrieve images."))
return images
def get_volume_snapshots_data(self):
if base.is_service_enabled(self.request, 'volume'):
try:
snapshots = api.cinder.volume_snapshot_list(self.request)
volumes = api.cinder.volume_list(self.request)
volumes = dict((v.id, v) for v in volumes)
except Exception:
snapshots = []
volumes = {}
exceptions.handle(self.request, _("Unable to retrieve "
"volume snapshots."))
for snapshot in snapshots:
volume = volumes.get(snapshot.volume_id)
setattr(snapshot, '_volume', volume)
else:
snapshots = []
return snapshots
class DetailView(tabs.TabView):
tab_group_class = vol_snsh_tabs.SnapshotDetailTabs
template_name = 'project/images_and_snapshots/snapshots/detail.html'
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
context["snapshot"] = self.get_data()
return context
@memoized.memoized_method
def get_data(self):
try:
snapshot_id = self.kwargs['snapshot_id']
return api.cinder.volume_snapshot_get(self.request, snapshot_id)
except Exception:
url = reverse('horizon:project:images_and_snapshots:index')
exceptions.handle(self.request,
_('Unable to retrieve snapshot details.'),
redirect=url)
def get_tabs(self, request, *args, **kwargs):
snapshot = self.get_data()
return self.tab_group_class(request, snapshot=snapshot, **kwargs)

View File

@ -27,7 +27,7 @@ from horizon.utils import fields
from horizon.utils import validators from horizon.utils import validators
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.dashboards.project.images_and_snapshots import utils from openstack_dashboard.dashboards.project.images import utils
def _image_choice_title(img): def _image_choice_title(img):

View File

@ -290,7 +290,7 @@ class EditInstanceSecurityGroups(EditInstance):
class CreateSnapshot(tables.LinkAction): class CreateSnapshot(tables.LinkAction):
name = "snapshot" name = "snapshot"
verbose_name = _("Create Snapshot") verbose_name = _("Create Snapshot")
url = "horizon:project:images_and_snapshots:snapshots:create" url = "horizon:project:images:snapshots:create"
classes = ("ajax-modal", "btn-camera") classes = ("ajax-modal", "btn-camera")
policy_rules = (("compute", "compute:snapshot"),) policy_rules = (("compute", "compute:snapshot"),)

View File

@ -100,7 +100,7 @@
{% with default_key_name="<em>"|add:_("None")|add:"</em>" %} {% with default_key_name="<em>"|add:_("None")|add:"</em>" %}
<dd>{{ instance.key_name|default:default_key_name }}</dd> <dd>{{ instance.key_name|default:default_key_name }}</dd>
{% endwith %} {% endwith %}
{% url 'horizon:project:images_and_snapshots:images:detail' instance.image.id as image_url %} {% url 'horizon:project:images:images:detail' instance.image.id as image_url %}
<dt>{% trans "Image Name" %}</dt> <dt>{% trans "Image Name" %}</dt>
<dd><a href="{{ image_url }}">{{ instance.image_name }}</a></dd> <dd><a href="{{ image_url }}">{{ instance.image_name }}</a></dd>
{% with default_item_value="<em>"|add:_("N/A")|add:"</em>" %} {% with default_item_value="<em>"|add:_("N/A")|add:"</em>" %}

View File

@ -870,8 +870,6 @@ class InstanceTests(test.TestCase):
'server_list', 'server_list',
'flavor_list', 'flavor_list',
'server_delete'), 'server_delete'),
cinder: ('volume_snapshot_list',
'volume_list',),
api.glance: ('image_list_detailed',)}) api.glance: ('image_list_detailed',)})
def test_create_instance_snapshot(self): def test_create_instance_snapshot(self):
server = self.servers.first() server = self.servers.first()
@ -883,17 +881,15 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest), api.glance.image_list_detailed(IsA(http.HttpRequest),
marker=None).AndReturn([[], False]) marker=None).AndReturn([[], False])
cinder.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([])
cinder.volume_list(IsA(http.HttpRequest)).AndReturn([])
self.mox.ReplayAll() self.mox.ReplayAll()
formData = {'instance_id': server.id, formData = {'instance_id': server.id,
'method': 'CreateSnapshot', 'method': 'CreateSnapshot',
'name': 'snapshot1'} 'name': 'snapshot1'}
url = reverse('horizon:project:images_and_snapshots:snapshots:create', url = reverse('horizon:project:images:snapshots:create',
args=[server.id]) args=[server.id])
redir_url = reverse('horizon:project:images_and_snapshots:index') redir_url = reverse('horizon:project:images:index')
res = self.client.post(url, formData) res = self.client.post(url, formData)
self.assertRedirects(res, redir_url) self.assertRedirects(res, redir_url)

View File

@ -40,7 +40,7 @@ from openstack_dashboard.api import base
from openstack_dashboard.api import cinder from openstack_dashboard.api import cinder
from openstack_dashboard.usage import quotas from openstack_dashboard.usage import quotas
from openstack_dashboard.dashboards.project.images_and_snapshots import utils from openstack_dashboard.dashboards.project.images import utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)

View File

@ -27,7 +27,7 @@ from openstack_dashboard.api import base
from openstack_dashboard.api import cinder from openstack_dashboard.api import cinder
from openstack_dashboard.dashboards.project.volumes \ from openstack_dashboard.dashboards.project.volumes \
import tables as volume_tables .volumes import tables as volume_tables
class DeleteVolumeSnapshot(tables.DeleteAction): class DeleteVolumeSnapshot(tables.DeleteAction):
@ -51,7 +51,7 @@ class DeleteVolumeSnapshot(tables.DeleteAction):
class CreateVolumeFromSnapshot(tables.LinkAction): class CreateVolumeFromSnapshot(tables.LinkAction):
name = "create_from_snapshot" name = "create_from_snapshot"
verbose_name = _("Create Volume") verbose_name = _("Create Volume")
url = "horizon:project:volumes:create" url = "horizon:project:volumes:volumes:create"
classes = ("ajax-modal", "btn-camera") classes = ("ajax-modal", "btn-camera")
policy_rules = (("volume", "volume:create"),) policy_rules = (("volume", "volume:create"),)
@ -95,10 +95,11 @@ class SnapshotVolumeNameColumn(tables.Column):
class VolumeSnapshotsTable(volume_tables.VolumesTableBase): class VolumeSnapshotsTable(volume_tables.VolumesTableBase):
name = tables.Column("display_name", name = tables.Column("display_name",
verbose_name=_("Name"), verbose_name=_("Name"),
link="horizon:project:images_and_snapshots:detail") link="horizon:project:volumes:detail")
volume_name = SnapshotVolumeNameColumn("display_name", volume_name = SnapshotVolumeNameColumn(
verbose_name=_("Volume Name"), "display_name",
link="horizon:project:volumes:detail") verbose_name=_("Volume Name"),
link="horizon:project:volumes:volumes:detail")
class Meta: class Meta:
name = "volume_snapshots" name = "volume_snapshots"

View File

@ -26,15 +26,14 @@ from openstack_dashboard.api import cinder
class OverviewTab(tabs.Tab): class OverviewTab(tabs.Tab):
name = _("Overview") name = _("Overview")
slug = "overview" slug = "overview"
template_name = ("project/images_and_snapshots/snapshots/" template_name = ("project/volumes/snapshots/_detail_overview.html")
"_detail_overview.html")
def get_context_data(self, request): def get_context_data(self, request):
try: try:
snapshot = self.tab_group.kwargs['snapshot'] snapshot = self.tab_group.kwargs['snapshot']
volume = cinder.volume_get(request, snapshot.volume_id) volume = cinder.volume_get(request, snapshot.volume_id)
except Exception: except Exception:
redirect = reverse('horizon:project:images_and_snapshots:index') redirect = reverse('horizon:project:volumes:index')
exceptions.handle(self.request, exceptions.handle(self.request,
_('Unable to retrieve snapshot details.'), _('Unable to retrieve snapshot details.'),
redirect=redirect) redirect=redirect)

View File

@ -28,7 +28,7 @@ from openstack_dashboard.test import helpers as test
from openstack_dashboard.usage import quotas from openstack_dashboard.usage import quotas
INDEX_URL = reverse('horizon:project:images_and_snapshots:index') INDEX_URL = reverse('horizon:project:volumes:index')
class VolumeSnapshotsViewTests(test.TestCase): class VolumeSnapshotsViewTests(test.TestCase):
@ -46,11 +46,12 @@ class VolumeSnapshotsViewTests(test.TestCase):
AndReturn(usage_limit) AndReturn(usage_limit)
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:create_snapshot', url = reverse('horizon:project:volumes:'
args=[volume.id]) 'volumes:create_snapshot', args=[volume.id])
res = self.client.get(url) res = self.client.get(url)
self.assertTemplateUsed(res, 'project/volumes/create_snapshot.html') self.assertTemplateUsed(res, 'project/volumes/volumes/'
'create_snapshot.html')
@test.create_stubs({cinder: ('volume_get', @test.create_stubs({cinder: ('volume_get',
'volume_snapshot_create',)}) 'volume_snapshot_create',)})
@ -73,7 +74,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
'volume_id': volume.id, 'volume_id': volume.id,
'name': snapshot.display_name, 'name': snapshot.display_name,
'description': snapshot.display_description} 'description': snapshot.display_description}
url = reverse('horizon:project:volumes:create_snapshot', url = reverse('horizon:project:volumes:volumes:create_snapshot',
args=[volume.id]) args=[volume.id])
res = self.client.post(url, formData) res = self.client.post(url, formData)
self.assertRedirectsNoFollow(res, INDEX_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
@ -99,12 +100,12 @@ class VolumeSnapshotsViewTests(test.TestCase):
'volume_id': volume.id, 'volume_id': volume.id,
'name': snapshot.display_name, 'name': snapshot.display_name,
'description': snapshot.display_description} 'description': snapshot.display_description}
url = reverse('horizon:project:volumes:create_snapshot', url = reverse('horizon:project:volumes:volumes:create_snapshot',
args=[volume.id]) args=[volume.id])
res = self.client.post(url, formData) res = self.client.post(url, formData)
self.assertRedirectsNoFollow(res, INDEX_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api.glance: ('image_list_detailed',), @test.create_stubs({api.nova: ('server_list',),
api.cinder: ('volume_snapshot_list', api.cinder: ('volume_snapshot_list',
'volume_list', 'volume_list',
'volume_snapshot_delete')}) 'volume_snapshot_delete')})
@ -113,20 +114,20 @@ class VolumeSnapshotsViewTests(test.TestCase):
volumes = self.volumes.list() volumes = self.volumes.list()
snapshot = self.volume_snapshots.first() snapshot = self.volume_snapshots.first()
api.glance.image_list_detailed(IsA(http.HttpRequest),
marker=None).AndReturn(([], False))
api.cinder.volume_snapshot_list(IsA(http.HttpRequest)). \ api.cinder.volume_snapshot_list(IsA(http.HttpRequest)). \
AndReturn(vol_snapshots) AndReturn(vol_snapshots)
api.cinder.volume_list(IsA(http.HttpRequest)) \ api.cinder.volume_list(IsA(http.HttpRequest)). \
.AndReturn(volumes) AndReturn(volumes)
api.cinder.volume_snapshot_delete(IsA(http.HttpRequest), snapshot.id) api.cinder.volume_snapshot_delete(IsA(http.HttpRequest), snapshot.id)
api.glance.image_list_detailed(IsA(http.HttpRequest), api.cinder.volume_list(IsA(http.HttpRequest), search_opts=None). \
marker=None).AndReturn(([], False)) AndReturn(volumes)
api.nova.server_list(IsA(http.HttpRequest), search_opts=None). \
AndReturn([self.servers.list(), False])
api.cinder.volume_snapshot_list(IsA(http.HttpRequest)). \ api.cinder.volume_snapshot_list(IsA(http.HttpRequest)). \
AndReturn([]) AndReturn([])
api.cinder.volume_list(IsA(http.HttpRequest)) \ api.cinder.volume_list(IsA(http.HttpRequest)). \
.AndReturn(volumes) AndReturn(volumes)
self.mox.ReplayAll() self.mox.ReplayAll()
formData = {'action': formData = {'action':
@ -148,7 +149,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:images_and_snapshots:detail', url = reverse('horizon:project:volumes:detail',
args=[snapshot.id]) args=[snapshot.id])
res = self.client.get(url) res = self.client.get(url)
@ -172,7 +173,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
AndRaise(self.exceptions.cinder) AndRaise(self.exceptions.cinder)
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:images_and_snapshots:detail', url = reverse('horizon:project:volumes:detail',
args=[snapshot.id]) args=[snapshot.id])
res = self.client.get(url) res = self.client.get(url)
@ -191,7 +192,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:images_and_snapshots:detail', url = reverse('horizon:project:volumes:detail',
args=[snapshot.id]) args=[snapshot.id])
res = self.client.get(url) res = self.client.get(url)

View File

@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4 # vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 Nebula, Inc. # Copyright 2013 Nebula, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -14,21 +14,98 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tabs from horizon import tabs
from openstack_dashboard import api
class OverviewTab(tabs.Tab): from openstack_dashboard.dashboards.project.volumes.snapshots \
name = _("Overview") import tables as vol_snapshot_tables
slug = "overview" from openstack_dashboard.dashboards.project.volumes.volumes \
template_name = ("project/volumes/" import tables as volume_tables
"_detail_overview.html")
def get_context_data(self, request):
return {"volume": self.tab_group.kwargs['volume']}
class VolumeDetailTabs(tabs.TabGroup): class VolumeTableMixIn(object):
slug = "volume_details" def _get_volumes(self, search_opts=None):
tabs = (OverviewTab,) try:
return api.cinder.volume_list(self.request,
search_opts=search_opts)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve volume list.'))
return []
def _get_instances(self, search_opts=None):
try:
instances, has_more = api.nova.server_list(self.request,
search_opts=search_opts)
return instances
except Exception:
exceptions.handle(self.request,
_("Unable to retrieve volume/instance "
"attachment information"))
return []
def _set_id_if_nameless(self, volumes):
for volume in volumes:
# It is possible to create a volume with no name through the
# EC2 API, use the ID in those cases.
if not volume.display_name:
volume.display_name = volume.id
def _set_attachments_string(self, volumes, instances):
instances = SortedDict([(inst.id, inst) for inst in instances])
for volume in volumes:
for att in volume.attachments:
server_id = att.get('server_id', None)
att['instance'] = instances.get(server_id, None)
class VolumeTab(tabs.TableTab, VolumeTableMixIn):
table_classes = (volume_tables.VolumesTable,)
name = _("Volumes")
slug = "volumes_tab"
template_name = ("horizon/common/_detail_table.html")
def get_volumes_data(self):
volumes = self._get_volumes()
instances = self._get_instances()
self._set_id_if_nameless(volumes)
self._set_attachments_string(volumes, instances)
return volumes
class SnapshotTab(tabs.TableTab):
table_classes = (vol_snapshot_tables.VolumeSnapshotsTable,)
name = _("Volume Snapshots")
slug = "snapshots_tab"
template_name = ("horizon/common/_detail_table.html")
def get_volume_snapshots_data(self):
if api.base.is_service_enabled(self.request, 'volume'):
try:
snapshots = api.cinder.volume_snapshot_list(self.request)
volumes = api.cinder.volume_list(self.request)
volumes = dict((v.id, v) for v in volumes)
except Exception:
snapshots = []
volumes = {}
exceptions.handle(self.request, _("Unable to retrieve "
"volume snapshots."))
for snapshot in snapshots:
volume = volumes.get(snapshot.volume_id)
setattr(snapshot, '_volume', volume)
else:
snapshots = []
return snapshots
class VolumeAndSnapshotTabs(tabs.TabGroup):
slug = "volumes_and_snapshots"
tabs = (VolumeTab, SnapshotTab,)
sticky = True

View File

@ -1,11 +1,15 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Volumes" %}{% endblock %} {% block title %}{% trans "Volumes &amp; Snapshots" %}{% endblock %}
{% block page_header %} {% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Volumes") %} {% include "horizon/common/_page_header.html" with title=_("Volumes &amp; Snapshots")%}
{% endblock page_header %} {% endblock page_header %}
{% block main %} {% block main %}
{{ table.render }} <div class="row-fluid">
<div class="span12">
{{ tab_group.render }}
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -19,7 +19,7 @@
<dd>{{ snapshot.status|capfirst }}</dd> <dd>{{ snapshot.status|capfirst }}</dd>
<dt>{% trans "Volume" %}</dt> <dt>{% trans "Volume" %}</dt>
<dd> <dd>
<a href="{% url 'horizon:project:volumes:detail' snapshot.volume_id %}"> <a href="{% url 'horizon:project:volumes:volumes:detail' snapshot.volume_id %}">
{% if volume.display_name %} {% if volume.display_name %}
{{ volume.display_name }} {{ volume.display_name }}
{% else %} {% else %}

View File

@ -3,7 +3,7 @@
{% load url from future %} {% load url from future %}
{% block form_id %}attach_volume_form{% endblock %} {% block form_id %}attach_volume_form{% endblock %}
{% block form_action %}{% url 'horizon:project:volumes:attach' volume.id %}{% endblock %} {% block form_action %}{% url 'horizon:project:volumes:volumes:attach' volume.id %}{% endblock %}
{% block form_class %}{{ block.super }} horizontal {% if show_attach %}split_half{% else %} no_split{% endif %}{% endblock %} {% block form_class %}{{ block.super }} horizontal {% if show_attach %}split_half{% else %} no_split{% endif %}{% endblock %}
{% block modal_id %}attach_volume_modal{% endblock %} {% block modal_id %}attach_volume_modal{% endblock %}

View File

@ -3,7 +3,7 @@
{% load url from future %} {% load url from future %}
{% block form_id %}{% endblock %} {% block form_id %}{% endblock %}
{% block form_action %}{% url 'horizon:project:volumes:create' %}?{{ request.GET.urlencode }}{% endblock %} {% block form_action %}{% url 'horizon:project:volumes:volumes:create' %}?{{ request.GET.urlencode }}{% endblock %}
{% block modal_id %}create_volume_modal{% endblock %} {% block modal_id %}create_volume_modal{% endblock %}
{% block modal-header %}{% trans "Create Volume" %}{% endblock %} {% block modal-header %}{% trans "Create Volume" %}{% endblock %}
@ -16,7 +16,7 @@
</div> </div>
<div class="right quota-dynamic"> <div class="right quota-dynamic">
{% include "project/volumes/_limits.html" with usages=usages %} {% include "project/volumes/volumes/_limits.html" with usages=usages %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -3,7 +3,7 @@
{% load url from future %} {% load url from future %}
{% block form_id %}{% endblock %} {% block form_id %}{% endblock %}
{% block form_action %}{% url 'horizon:project:volumes:create_snapshot' volume_id %}{% endblock %} {% block form_action %}{% url 'horizon:project:volumes:volumes:create_snapshot' volume_id %}{% endblock %}
{% block modal_id %}create_volume_snapshot_modal{% endblock %} {% block modal_id %}create_volume_snapshot_modal{% endblock %}
{% block modal-header %}{% trans "Create Volume Snapshot" %}{% endblock %} {% block modal-header %}{% trans "Create Volume Snapshot" %}{% endblock %}
@ -15,7 +15,7 @@
</fieldset> </fieldset>
</div> </div>
<div class="right quota-dynamic"> <div class="right quota-dynamic">
{% include "project/volumes/_limits.html" with usages=usages snapshot_quota=True %} {% include "project/volumes/volumes/_limits.html" with usages=usages snapshot_quota=True %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -3,7 +3,7 @@
{% load url from future %} {% load url from future %}
{% block form_id %}{% endblock %} {% block form_id %}{% endblock %}
{% block form_action %}{% url 'horizon:project:volumes:extend' volume.id%}{% endblock %} {% block form_action %}{% url 'horizon:project:volumes:volumes:extend' volume.id%}{% endblock %}
{% block modal_id %}extend_volume_modal{% endblock %} {% block modal_id %}extend_volume_modal{% endblock %}
{% block modal-header %}{% trans "Extend Volume" %}{% endblock %} {% block modal-header %}{% trans "Extend Volume" %}{% endblock %}
@ -16,7 +16,7 @@
</div> </div>
<div class="right quota-dynamic"> <div class="right quota-dynamic">
{% include "project/volumes/_extend_limits.html" with usages=usages %} {% include "project/volumes/volumes/_extend_limits.html" with usages=usages %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -3,7 +3,7 @@
{% load url from future %} {% load url from future %}
{% block form_id %}{% endblock %} {% block form_id %}{% endblock %}
{% block form_action %}{% url 'horizon:project:volumes:update' volume.id %}{% endblock %} {% block form_action %}{% url 'horizon:project:volumes:volumes:update' volume.id %}{% endblock %}
{% block modal_id %}update_volume_modal{% endblock %} {% block modal_id %}update_volume_modal{% endblock %}
{% block modal-header %}{% trans "Edit Volume" %}{% endblock %} {% block modal-header %}{% trans "Edit Volume" %}{% endblock %}

View File

@ -7,5 +7,5 @@
{% endblock page_header %} {% endblock page_header %}
{% block main %} {% block main %}
{% include 'project/volumes/_attach.html' %} {% include 'project/volumes/volumes/_attach.html' %}
{% endblock %} {% endblock %}

View File

@ -7,5 +7,5 @@
{% endblock page_header %} {% endblock page_header %}
{% block main %} {% block main %}
{% include 'project/volumes/_create.html' %} {% include 'project/volumes/volumes/_create.html' %}
{% endblock %} {% endblock %}

View File

@ -7,5 +7,5 @@
{% endblock page_header %} {% endblock page_header %}
{% block main %} {% block main %}
{% include 'project/volumes/_create_snapshot.html' %} {% include 'project/volumes/volumes/_create_snapshot.html' %}
{% endblock %} {% endblock %}

View File

@ -7,5 +7,5 @@
{% endblock page_header %} {% endblock page_header %}
{% block main %} {% block main %}
{% include 'project/volumes/_extend.html' %} {% include 'project/volumes/volumes/_extend.html' %}
{% endblock %} {% endblock %}

View File

@ -7,5 +7,5 @@
{% endblock page_header %} {% endblock page_header %}
{% block main %} {% block main %}
{% include 'project/volumes/_update.html' %} {% include 'project/volumes/volumes/_update.html' %}
{% endblock %} {% endblock %}

View File

@ -0,0 +1,48 @@
# 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.
from django.core.urlresolvers import reverse
from django import http
from mox import IsA # noqa
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
INDEX_URL = reverse('horizon:project:volumes:index')
class VolumeAndSnapshotsTests(test.TestCase):
@test.create_stubs({api.cinder: ('volume_list',
'volume_snapshot_list',),
api.nova: ('server_list',)})
def test_index(self):
vol_snaps = self.volume_snapshots.list()
volumes = self.volumes.list()
api.cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\
AndReturn(volumes)
api.nova.server_list(IsA(http.HttpRequest), search_opts=None).\
AndReturn([self.servers.list(), False])
api.cinder.volume_snapshot_list(IsA(http.HttpRequest)).\
AndReturn(vol_snaps)
api.cinder.volume_list(IsA(http.HttpRequest)).AndReturn(volumes)
self.mox.ReplayAll()
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'project/volumes/index.html')

View File

@ -14,28 +14,19 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from django.conf.urls import include # noqa
from django.conf.urls import patterns # noqa from django.conf.urls import patterns # noqa
from django.conf.urls import url # noqa from django.conf.urls import url # noqa
from openstack_dashboard.dashboards.project.volumes import views from openstack_dashboard.dashboards.project.volumes import views
from openstack_dashboard.dashboards.project.volumes.volumes \
import urls as volume_urls
urlpatterns = patterns('openstack_dashboard.dashboards.project.volumes.views', urlpatterns = patterns('',
url(r'^$', views.IndexView.as_view(), name='index'), url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create/$', views.CreateView.as_view(), name='create'), url(r'', include(volume_urls, namespace='volumes')),
url(r'^(?P<volume_id>[^/]+)/extend/$', url(r'^snapshots/(?P<snapshot_id>[^/]+)/$',
views.ExtendView.as_view(),
name='extend'),
url(r'^(?P<volume_id>[^/]+)/attach/$',
views.EditAttachmentsView.as_view(),
name='attach'),
url(r'^(?P<volume_id>[^/]+)/create_snapshot/$',
views.CreateSnapshotView.as_view(),
name='create_snapshot'),
url(r'^(?P<volume_id>[^/]+)/$',
views.DetailView.as_view(), views.DetailView.as_view(),
name='detail'), name='detail'),
url(r'^(?P<volume_id>[^/]+)/update/$',
views.UpdateView.as_view(),
name='update'),
) )

View File

@ -14,279 +14,47 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
"""
Views for managing volumes.
"""
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions from horizon import exceptions
from horizon import forms
from horizon import tables
from horizon import tabs from horizon import tabs
from horizon.utils import memoized from horizon.utils import memoized
from openstack_dashboard import api
from openstack_dashboard.api import cinder from openstack_dashboard.api import cinder
from openstack_dashboard.usage import quotas
from openstack_dashboard.dashboards.project.volumes \
import forms as project_forms
from openstack_dashboard.dashboards.project.volumes \
import tables as project_tables
from openstack_dashboard.dashboards.project.volumes \ from openstack_dashboard.dashboards.project.volumes \
import tabs as project_tabs import tabs as project_tabs
from openstack_dashboard.dashboards.project.volumes \
.snapshots import tabs as vol_snapshot_tabs
class VolumeTableMixIn(object): class IndexView(tabs.TabbedTableView):
def _get_volumes(self, search_opts=None): tab_group_class = project_tabs.VolumeAndSnapshotTabs
try:
return cinder.volume_list(self.request, search_opts=search_opts)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve volume list.'))
return []
def _get_instances(self, search_opts=None):
try:
instances, has_more = api.nova.server_list(self.request,
search_opts=search_opts)
return instances
except Exception:
exceptions.handle(self.request,
_("Unable to retrieve volume/instance "
"attachment information"))
return []
def _set_id_if_nameless(self, volumes):
for volume in volumes:
# It is possible to create a volume with no name through the
# EC2 API, use the ID in those cases.
if not volume.display_name:
volume.display_name = volume.id
def _set_attachments_string(self, volumes, instances):
instances = SortedDict([(inst.id, inst) for inst in instances])
for volume in volumes:
for att in volume.attachments:
server_id = att.get('server_id', None)
att['instance'] = instances.get(server_id, None)
class IndexView(tables.DataTableView, VolumeTableMixIn):
table_class = project_tables.VolumesTable
template_name = 'project/volumes/index.html' template_name = 'project/volumes/index.html'
def get_data(self):
volumes = self._get_volumes()
instances = self._get_instances()
self._set_id_if_nameless(volumes)
self._set_attachments_string(volumes, instances)
return volumes
class DetailView(tabs.TabView): class DetailView(tabs.TabView):
tab_group_class = project_tabs.VolumeDetailTabs tab_group_class = vol_snapshot_tabs.SnapshotDetailTabs
template_name = 'project/volumes/detail.html' template_name = 'project/volumes/snapshots/detail.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs) context = super(DetailView, self).get_context_data(**kwargs)
context["volume"] = self.get_data() context["snapshot"] = self.get_data()
return context return context
@memoized.memoized_method @memoized.memoized_method
def get_data(self): def get_data(self):
try: try:
volume_id = self.kwargs['volume_id'] snapshot_id = self.kwargs['snapshot_id']
volume = cinder.volume_get(self.request, volume_id) snapshot = cinder.volume_snapshot_get(self.request, snapshot_id)
for att in volume.attachments:
att['instance'] = api.nova.server_get(self.request,
att['server_id'])
except Exception: except Exception:
redirect = reverse('horizon:project:volumes:index') redirect = reverse('horizon:project:volumes:index')
exceptions.handle(self.request, exceptions.handle(self.request,
_('Unable to retrieve volume details.'), _('Unable to retrieve snapshot details.'),
redirect=redirect) redirect=redirect)
return volume return snapshot
def get_tabs(self, request, *args, **kwargs): def get_tabs(self, request, *args, **kwargs):
volume = self.get_data() snapshot = self.get_data()
return self.tab_group_class(request, volume=volume, **kwargs) return self.tab_group_class(request, snapshot=snapshot, **kwargs)
class CreateView(forms.ModalFormView):
form_class = project_forms.CreateForm
template_name = 'project/volumes/create.html'
success_url = reverse_lazy("horizon:project:volumes:index")
def get_context_data(self, **kwargs):
context = super(CreateView, self).get_context_data(**kwargs)
try:
context['usages'] = quotas.tenant_limit_usages(self.request)
except Exception:
exceptions.handle(self.request)
return context
class ExtendView(forms.ModalFormView):
form_class = project_forms.ExtendForm
template_name = 'project/volumes/extend.html'
success_url = reverse_lazy("horizon:project:volumes:index")
def get_object(self):
if not hasattr(self, "_object"):
volume_id = self.kwargs['volume_id']
try:
self._object = cinder.volume_get(self.request, volume_id)
except Exception:
self._object = None
exceptions.handle(self.request,
_('Unable to retrieve volume information.'))
return self._object
def get_context_data(self, **kwargs):
context = super(ExtendView, self).get_context_data(**kwargs)
context['volume'] = self.get_object()
try:
usages = quotas.tenant_limit_usages(self.request)
usages['gigabytesUsed'] = (usages['gigabytesUsed']
- context['volume'].size)
context['usages'] = usages
except Exception:
exceptions.handle(self.request)
return context
def get_initial(self):
volume = self.get_object()
return {'id': self.kwargs['volume_id'],
'name': volume.display_name,
'orig_size': volume.size}
class CreateSnapshotView(forms.ModalFormView):
form_class = project_forms.CreateSnapshotForm
template_name = 'project/volumes/create_snapshot.html'
success_url = reverse_lazy("horizon:project:images_and_snapshots:index")
def get_context_data(self, **kwargs):
context = super(CreateSnapshotView, self).get_context_data(**kwargs)
context['volume_id'] = self.kwargs['volume_id']
try:
volume = cinder.volume_get(self.request, context['volume_id'])
if (volume.status == 'in-use'):
context['attached'] = True
context['form'].set_warning(_("This volume is currently "
"attached to an instance. "
"In some cases, creating a "
"snapshot from an attached "
"volume can result in a "
"corrupted snapshot."))
context['usages'] = quotas.tenant_limit_usages(self.request)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve volume information.'))
return context
def get_initial(self):
return {'volume_id': self.kwargs["volume_id"]}
class UpdateView(forms.ModalFormView):
form_class = project_forms.UpdateForm
template_name = 'project/volumes/update.html'
success_url = reverse_lazy("horizon:project:volumes:index")
def get_object(self):
if not hasattr(self, "_object"):
vol_id = self.kwargs['volume_id']
try:
self._object = cinder.volume_get(self.request, vol_id)
except Exception:
msg = _('Unable to retrieve volume.')
url = reverse('horizon:project:volumes:index')
exceptions.handle(self.request, msg, redirect=url)
return self._object
def get_context_data(self, **kwargs):
context = super(UpdateView, self).get_context_data(**kwargs)
context['volume'] = self.get_object()
return context
def get_initial(self):
volume = self.get_object()
return {'volume_id': self.kwargs["volume_id"],
'name': volume.display_name,
'description': volume.display_description}
class EditAttachmentsView(tables.DataTableView, forms.ModalFormView):
table_class = project_tables.AttachmentsTable
form_class = project_forms.AttachForm
template_name = 'project/volumes/attach.html'
success_url = reverse_lazy("horizon:project:volumes:index")
@memoized.memoized_method
def get_object(self):
volume_id = self.kwargs['volume_id']
try:
return cinder.volume_get(self.request, volume_id)
except Exception:
self._object = None
exceptions.handle(self.request,
_('Unable to retrieve volume information.'))
def get_data(self):
try:
volumes = self.get_object()
attachments = [att for att in volumes.attachments if att]
except Exception:
attachments = []
exceptions.handle(self.request,
_('Unable to retrieve volume information.'))
return attachments
def get_initial(self):
try:
instances, has_more = api.nova.server_list(self.request)
except Exception:
instances = []
exceptions.handle(self.request,
_("Unable to retrieve attachment information."))
return {'volume': self.get_object(),
'instances': instances}
@memoized.memoized_method
def get_form(self):
form_class = self.get_form_class()
return super(EditAttachmentsView, self).get_form(form_class)
def get_context_data(self, **kwargs):
context = super(EditAttachmentsView, self).get_context_data(**kwargs)
context['form'] = self.get_form()
volume = self.get_object()
if volume and volume.status == 'available':
context['show_attach'] = True
else:
context['show_attach'] = False
context['volume'] = volume
if self.request.is_ajax():
context['hide'] = True
return context
def get(self, request, *args, **kwargs):
# Table action handling
handled = self.construct_tables()
if handled:
return handled
return self.render_to_response(self.get_context_data(**kwargs))
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.get(request, *args, **kwargs)

View File

@ -35,7 +35,7 @@ from horizon.utils.memoized import memoized # noqa
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.api import cinder from openstack_dashboard.api import cinder
from openstack_dashboard.api import glance from openstack_dashboard.api import glance
from openstack_dashboard.dashboards.project.images_and_snapshots import utils from openstack_dashboard.dashboards.project.images import utils
from openstack_dashboard.dashboards.project.instances import tables from openstack_dashboard.dashboards.project.instances import tables
from openstack_dashboard.usage import quotas from openstack_dashboard.usage import quotas
@ -466,7 +466,7 @@ class CreateSnapshotForm(forms.SelfHandlingForm):
messages.info(request, message) messages.info(request, message)
return snapshot return snapshot
except Exception: except Exception:
redirect = reverse("horizon:project:images_and_snapshots:index") redirect = reverse("horizon:project:volumes:index")
exceptions.handle(request, exceptions.handle(request,
_('Unable to create volume snapshot.'), _('Unable to create volume snapshot.'),
redirect=redirect) redirect=redirect)

View File

@ -67,7 +67,7 @@ class DeleteVolume(tables.DeleteAction):
class CreateVolume(tables.LinkAction): class CreateVolume(tables.LinkAction):
name = "create" name = "create"
verbose_name = _("Create Volume") verbose_name = _("Create Volume")
url = "horizon:project:volumes:create" url = "horizon:project:volumes:volumes:create"
classes = ("ajax-modal", "btn-create") classes = ("ajax-modal", "btn-create")
policy_rules = (("volume", "volume:create"),) policy_rules = (("volume", "volume:create"),)
@ -89,7 +89,7 @@ class CreateVolume(tables.LinkAction):
class ExtendVolume(tables.LinkAction): class ExtendVolume(tables.LinkAction):
name = "extend" name = "extend"
verbose_name = _("Extend Volume") verbose_name = _("Extend Volume")
url = "horizon:project:volumes:extend" url = "horizon:project:volumes:volumes:extend"
classes = ("ajax-modal", "btn-extend") classes = ("ajax-modal", "btn-extend")
policy_rules = (("volume", "volume:extend"),) policy_rules = (("volume", "volume:extend"),)
@ -106,7 +106,7 @@ class ExtendVolume(tables.LinkAction):
class EditAttachments(tables.LinkAction): class EditAttachments(tables.LinkAction):
name = "attachments" name = "attachments"
verbose_name = _("Edit Attachments") verbose_name = _("Edit Attachments")
url = "horizon:project:volumes:attach" url = "horizon:project:volumes:volumes:attach"
classes = ("ajax-modal", "btn-edit") classes = ("ajax-modal", "btn-edit")
def allowed(self, request, volume=None): def allowed(self, request, volume=None):
@ -129,7 +129,7 @@ class EditAttachments(tables.LinkAction):
class CreateSnapshot(tables.LinkAction): class CreateSnapshot(tables.LinkAction):
name = "snapshots" name = "snapshots"
verbose_name = _("Create Snapshot") verbose_name = _("Create Snapshot")
url = "horizon:project:volumes:create_snapshot" url = "horizon:project:volumes:volumes:create_snapshot"
classes = ("ajax-modal", "btn-camera") classes = ("ajax-modal", "btn-camera")
policy_rules = (("volume", "volume:create_snapshot"),) policy_rules = (("volume", "volume:create_snapshot"),)
@ -146,7 +146,7 @@ class CreateSnapshot(tables.LinkAction):
class EditVolume(tables.LinkAction): class EditVolume(tables.LinkAction):
name = "edit" name = "edit"
verbose_name = _("Edit Volume") verbose_name = _("Edit Volume")
url = "horizon:project:volumes:update" url = "horizon:project:volumes:volumes:update"
classes = ("ajax-modal", "btn-edit") classes = ("ajax-modal", "btn-edit")
policy_rules = (("volume", "volume:update"),) policy_rules = (("volume", "volume:update"),)
@ -228,7 +228,7 @@ class VolumesTableBase(tables.DataTable):
) )
name = tables.Column("display_name", name = tables.Column("display_name",
verbose_name=_("Name"), verbose_name=_("Name"),
link="horizon:project:volumes:detail") link="horizon:project:volumes:volumes:detail")
description = tables.Column("display_description", description = tables.Column("display_description",
verbose_name=_("Description"), verbose_name=_("Description"),
truncate=40) truncate=40)
@ -257,7 +257,7 @@ class VolumesFilterAction(tables.FilterAction):
class VolumesTable(VolumesTableBase): class VolumesTable(VolumesTableBase):
name = tables.Column("display_name", name = tables.Column("display_name",
verbose_name=_("Name"), verbose_name=_("Name"),
link="horizon:project:volumes:detail") link="horizon:project:volumes:volumes:detail")
volume_type = tables.Column(get_volume_type, volume_type = tables.Column(get_volume_type,
verbose_name=_("Type"), verbose_name=_("Type"),
empty_value="-") empty_value="-")

View File

@ -0,0 +1,33 @@
# 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.
from django.utils.translation import ugettext_lazy as _
from horizon import tabs
class OverviewTab(tabs.Tab):
name = _("Overview")
slug = "overview"
template_name = ("project/volumes/volumes/_detail_overview.html")
def get_context_data(self, request):
return {"volume": self.tab_group.kwargs['volume']}
class VolumeDetailTabs(tabs.TabGroup):
slug = "volume_details"
tabs = (OverviewTab,)

View File

@ -27,7 +27,8 @@ from mox import IsA # noqa
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.api import cinder from openstack_dashboard.api import cinder
from openstack_dashboard.dashboards.project.volumes import tables from openstack_dashboard.dashboards.project.volumes \
.volumes import tables
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
from openstack_dashboard.usage import quotas from openstack_dashboard.usage import quotas
@ -96,7 +97,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:create') url = reverse('horizon:project:volumes:volumes:create')
res = self.client.post(url, formData) res = self.client.post(url, formData)
redirect_url = reverse('horizon:project:volumes:index') redirect_url = reverse('horizon:project:volumes:index')
@ -160,7 +161,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:create') url = reverse('horizon:project:volumes:volumes:create')
res = self.client.post(url, formData) res = self.client.post(url, formData)
redirect_url = reverse('horizon:project:volumes:index') redirect_url = reverse('horizon:project:volumes:index')
@ -207,7 +208,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
# get snapshot from url # get snapshot from url
url = reverse('horizon:project:volumes:create') url = reverse('horizon:project:volumes:volumes:create')
res = self.client.post("?".join([url, res = self.client.post("?".join([url,
"snapshot_id=" + str(snapshot.id)]), "snapshot_id=" + str(snapshot.id)]),
formData) formData)
@ -276,7 +277,7 @@ class VolumeViewTests(test.TestCase):
source_volid=volume.id).AndReturn(volume) source_volid=volume.id).AndReturn(volume)
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:create') url = reverse('horizon:project:volumes:volumes:create')
redirect_url = reverse('horizon:project:volumes:index') redirect_url = reverse('horizon:project:volumes:index')
res = self.client.post(url, formData) res = self.client.post(url, formData)
self.assertNoFormErrors(res) self.assertNoFormErrors(res)
@ -346,7 +347,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
# get snapshot from dropdown list # get snapshot from dropdown list
url = reverse('horizon:project:volumes:create') url = reverse('horizon:project:volumes:volumes:create')
res = self.client.post(url, formData) res = self.client.post(url, formData)
redirect_url = reverse('horizon:project:volumes:index') redirect_url = reverse('horizon:project:volumes:index')
@ -382,7 +383,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:create') url = reverse('horizon:project:volumes:volumes:create')
res = self.client.post("?".join([url, res = self.client.post("?".join([url,
"snapshot_id=" + str(snapshot.id)]), "snapshot_id=" + str(snapshot.id)]),
formData, follow=True) formData, follow=True)
@ -437,7 +438,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
# get image from url # get image from url
url = reverse('horizon:project:volumes:create') url = reverse('horizon:project:volumes:volumes:create')
res = self.client.post("?".join([url, res = self.client.post("?".join([url,
"image_id=" + str(image.id)]), "image_id=" + str(image.id)]),
formData) formData)
@ -508,7 +509,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
# get image from dropdown list # get image from dropdown list
url = reverse('horizon:project:volumes:create') url = reverse('horizon:project:volumes:volumes:create')
res = self.client.post(url, formData) res = self.client.post(url, formData)
redirect_url = reverse('horizon:project:volumes:index') redirect_url = reverse('horizon:project:volumes:index')
@ -546,7 +547,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:create') url = reverse('horizon:project:volumes:volumes:create')
res = self.client.post("?".join([url, res = self.client.post("?".join([url,
"image_id=" + str(image.id)]), "image_id=" + str(image.id)]),
formData, follow=True) formData, follow=True)
@ -588,7 +589,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:create') url = reverse('horizon:project:volumes:volumes:create')
res = self.client.post("?".join([url, res = self.client.post("?".join([url,
"image_id=" + str(image.id)]), "image_id=" + str(image.id)]),
formData, follow=True) formData, follow=True)
@ -639,7 +640,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:create') url = reverse('horizon:project:volumes:volumes:create')
res = self.client.post(url, formData) res = self.client.post(url, formData)
expected_error = [u'A volume of 5000GB cannot be created as you only' expected_error = [u'A volume of 5000GB cannot be created as you only'
@ -688,7 +689,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:create') url = reverse('horizon:project:volumes:volumes:create')
res = self.client.post(url, formData) res = self.client.post(url, formData)
expected_error = [u'You are already using all of your available' expected_error = [u'You are already using all of your available'
@ -750,7 +751,6 @@ class VolumeViewTests(test.TestCase):
url = reverse('horizon:project:volumes:index') url = reverse('horizon:project:volumes:index')
res = self.client.post(url, formData, follow=True) res = self.client.post(url, formData, follow=True)
self.assertMessageCount(res, error=1)
self.assertEqual(list(res.context['messages'])[0].message, self.assertEqual(list(res.context['messages'])[0].message,
u'Unable to delete volume "%s". ' u'Unable to delete volume "%s". '
u'One or more snapshots depend on it.' % u'One or more snapshots depend on it.' %
@ -769,7 +769,8 @@ class VolumeViewTests(test.TestCase):
api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False]) api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False])
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:attach', args=[volume.id]) url = reverse('horizon:project:volumes:volumes:attach',
args=[volume.id])
res = self.client.get(url) res = self.client.get(url)
# Asserting length of 2 accounts for the one instance option, # Asserting length of 2 accounts for the one instance option,
# and the one 'Choose Instance' option. # and the one 'Choose Instance' option.
@ -792,7 +793,8 @@ class VolumeViewTests(test.TestCase):
api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False]) api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False])
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:attach', args=[volume.id]) url = reverse('horizon:project:volumes:volumes:attach',
args=[volume.id])
res = self.client.get(url) res = self.client.get(url)
# Assert the device field is hidden. # Assert the device field is hidden.
form = res.context['form'] form = res.context['form']
@ -815,7 +817,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:attach', url = reverse('horizon:project:volumes:volumes:attach',
args=[volume.id]) args=[volume.id])
res = self.client.get(url) res = self.client.get(url)
@ -874,7 +876,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:detail', url = reverse('horizon:project:volumes:volumes:detail',
args=[volume.id]) args=[volume.id])
res = self.client.get(url) res = self.client.get(url)
@ -925,7 +927,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:detail', url = reverse('horizon:project:volumes:volumes:detail',
args=[volume.id]) args=[volume.id])
res = self.client.get(url) res = self.client.get(url)
@ -948,7 +950,7 @@ class VolumeViewTests(test.TestCase):
'name': volume.display_name, 'name': volume.display_name,
'description': volume.display_description} 'description': volume.display_description}
url = reverse('horizon:project:volumes:update', url = reverse('horizon:project:volumes:volumes:update',
args=[volume.id]) args=[volume.id])
res = self.client.post(url, formData) res = self.client.post(url, formData)
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL) self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
@ -970,7 +972,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:extend', url = reverse('horizon:project:volumes:volumes:extend',
args=[volume.id]) args=[volume.id])
res = self.client.post(url, formData) res = self.client.post(url, formData)
@ -996,7 +998,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:volumes:extend', url = reverse('horizon:project:volumes:volumes:extend',
args=[volume.id]) args=[volume.id])
res = self.client.post(url, formData) res = self.client.post(url, formData)
self.assertFormError(res, 'form', None, self.assertFormError(res, 'form', None,

View File

@ -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.
from django.conf.urls import patterns # noqa
from django.conf.urls import url # noqa
from openstack_dashboard.dashboards.project.volumes \
.volumes import views
VIEWS_MOD = ('openstack_dashboard.dashboards.project.volumes.volumes.views')
urlpatterns = patterns(VIEWS_MOD,
url(r'^create/$', views.CreateView.as_view(), name='create'),
url(r'^(?P<volume_id>[^/]+)/extend/$',
views.ExtendView.as_view(),
name='extend'),
url(r'^(?P<volume_id>[^/]+)/attach/$',
views.EditAttachmentsView.as_view(),
name='attach'),
url(r'^(?P<volume_id>[^/]+)/create_snapshot/$',
views.CreateSnapshotView.as_view(),
name='create_snapshot'),
url(r'^(?P<volume_id>[^/]+)/$',
views.DetailView.as_view(),
name='detail'),
url(r'^(?P<volume_id>[^/]+)/update/$',
views.UpdateView.as_view(),
name='update'),
)

View File

@ -0,0 +1,244 @@
# 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.
"""
Views for managing volumes.
"""
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import tables
from horizon import tabs
from horizon.utils import memoized
from openstack_dashboard import api
from openstack_dashboard.api import cinder
from openstack_dashboard.usage import quotas
from openstack_dashboard.dashboards.project.volumes \
.volumes import forms as project_forms
from openstack_dashboard.dashboards.project.volumes \
.volumes import tables as project_tables
from openstack_dashboard.dashboards.project.volumes \
.volumes import tabs as project_tabs
class DetailView(tabs.TabView):
tab_group_class = project_tabs.VolumeDetailTabs
template_name = 'project/volumes/volumes/detail.html'
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
context["volume"] = self.get_data()
return context
@memoized.memoized_method
def get_data(self):
try:
volume_id = self.kwargs['volume_id']
volume = cinder.volume_get(self.request, volume_id)
for att in volume.attachments:
att['instance'] = api.nova.server_get(self.request,
att['server_id'])
except Exception:
redirect = reverse('horizon:project:volumes:index')
exceptions.handle(self.request,
_('Unable to retrieve volume details.'),
redirect=redirect)
return volume
def get_tabs(self, request, *args, **kwargs):
volume = self.get_data()
return self.tab_group_class(request, volume=volume, **kwargs)
class CreateView(forms.ModalFormView):
form_class = project_forms.CreateForm
template_name = 'project/volumes/volumes/create.html'
success_url = reverse_lazy("horizon:project:volumes:index")
def get_context_data(self, **kwargs):
context = super(CreateView, self).get_context_data(**kwargs)
try:
context['usages'] = quotas.tenant_limit_usages(self.request)
except Exception:
exceptions.handle(self.request)
return context
class ExtendView(forms.ModalFormView):
form_class = project_forms.ExtendForm
template_name = 'project/volumes/volumes/extend.html'
success_url = reverse_lazy("horizon:project:volumes:index")
def get_object(self):
if not hasattr(self, "_object"):
volume_id = self.kwargs['volume_id']
try:
self._object = cinder.volume_get(self.request, volume_id)
except Exception:
self._object = None
exceptions.handle(self.request,
_('Unable to retrieve volume information.'))
return self._object
def get_context_data(self, **kwargs):
context = super(ExtendView, self).get_context_data(**kwargs)
context['volume'] = self.get_object()
try:
usages = quotas.tenant_limit_usages(self.request)
usages['gigabytesUsed'] = (usages['gigabytesUsed']
- context['volume'].size)
context['usages'] = usages
except Exception:
exceptions.handle(self.request)
return context
def get_initial(self):
volume = self.get_object()
return {'id': self.kwargs['volume_id'],
'name': volume.display_name,
'orig_size': volume.size}
class CreateSnapshotView(forms.ModalFormView):
form_class = project_forms.CreateSnapshotForm
template_name = 'project/volumes/volumes/create_snapshot.html'
success_url = reverse_lazy("horizon:project:volumes:index")
def get_context_data(self, **kwargs):
context = super(CreateSnapshotView, self).get_context_data(**kwargs)
context['volume_id'] = self.kwargs['volume_id']
try:
volume = cinder.volume_get(self.request, context['volume_id'])
if (volume.status == 'in-use'):
context['attached'] = True
context['form'].set_warning(_("This volume is currently "
"attached to an instance. "
"In some cases, creating a "
"snapshot from an attached "
"volume can result in a "
"corrupted snapshot."))
context['usages'] = quotas.tenant_limit_usages(self.request)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve volume information.'))
return context
def get_initial(self):
return {'volume_id': self.kwargs["volume_id"]}
class UpdateView(forms.ModalFormView):
form_class = project_forms.UpdateForm
template_name = 'project/volumes/volumes/update.html'
success_url = reverse_lazy("horizon:project:volumes:index")
def get_object(self):
if not hasattr(self, "_object"):
vol_id = self.kwargs['volume_id']
try:
self._object = cinder.volume_get(self.request, vol_id)
except Exception:
msg = _('Unable to retrieve volume.')
url = reverse('horizon:project:volumes:index')
exceptions.handle(self.request, msg, redirect=url)
return self._object
def get_context_data(self, **kwargs):
context = super(UpdateView, self).get_context_data(**kwargs)
context['volume'] = self.get_object()
return context
def get_initial(self):
volume = self.get_object()
return {'volume_id': self.kwargs["volume_id"],
'name': volume.display_name,
'description': volume.display_description}
class EditAttachmentsView(tables.DataTableView, forms.ModalFormView):
table_class = project_tables.AttachmentsTable
form_class = project_forms.AttachForm
template_name = 'project/volumes/volumes/attach.html'
success_url = reverse_lazy("horizon:project:volumes:index")
@memoized.memoized_method
def get_object(self):
volume_id = self.kwargs['volume_id']
try:
return cinder.volume_get(self.request, volume_id)
except Exception:
self._object = None
exceptions.handle(self.request,
_('Unable to retrieve volume information.'))
def get_data(self):
try:
volumes = self.get_object()
attachments = [att for att in volumes.attachments if att]
except Exception:
attachments = []
exceptions.handle(self.request,
_('Unable to retrieve volume information.'))
return attachments
def get_initial(self):
try:
instances, has_more = api.nova.server_list(self.request)
except Exception:
instances = []
exceptions.handle(self.request,
_("Unable to retrieve attachment information."))
return {'volume': self.get_object(),
'instances': instances}
@memoized.memoized_method
def get_form(self):
form_class = self.get_form_class()
return super(EditAttachmentsView, self).get_form(form_class)
def get_context_data(self, **kwargs):
context = super(EditAttachmentsView, self).get_context_data(**kwargs)
context['form'] = self.get_form()
volume = self.get_object()
if volume and volume.status == 'available':
context['show_attach'] = True
else:
context['show_attach'] = False
context['volume'] = volume
if self.request.is_ajax():
context['hide'] = True
return context
def get(self, request, *args, **kwargs):
# Table action handling
handled = self.construct_tables()
if handled:
return handled
return self.render_to_response(self.get_context_data(**kwargs))
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.get(request, *args, **kwargs)

View File

@ -81,7 +81,7 @@ class NetworkProfile(tables.DataTable):
class EditPolicyProfile(tables.LinkAction): class EditPolicyProfile(tables.LinkAction):
name = "edit" name = "edit"
verbose_name = _("Edit Policy Profile") verbose_name = _("Edit Policy Profile")
url = "horizon:project:images_and_snapshots:images:update" url = "horizon:project:images:images:update"
classes = ("ajax-modal", "btn-edit") classes = ("ajax-modal", "btn-edit")