Merging Images and Snapshots into a single panel.
Change-Id: I897c6930a45b546dc3a742a4baf735fae4c7fbfd
This commit is contained in:
@@ -22,9 +22,10 @@ import horizon
|
||||
class Nova(horizon.Dashboard):
|
||||
name = "Dashboard"
|
||||
slug = "nova"
|
||||
panels = {_("Manage Compute"): ('overview', 'instances_and_volumes',
|
||||
'images', 'snapshots',
|
||||
'access_and_security',),
|
||||
panels = {_("Manage Compute"): ('overview',
|
||||
'instances_and_volumes',
|
||||
'access_and_security',
|
||||
'images_and_snapshots'),
|
||||
_("Network"): ('networks',),
|
||||
_("Object Store"): ('containers',)}
|
||||
default_panel = 'overview'
|
||||
|
||||
@@ -29,7 +29,7 @@ from horizon import api
|
||||
from horizon import test
|
||||
|
||||
|
||||
IMAGES_INDEX_URL = reverse('horizon:nova:images:index')
|
||||
IMAGES_INDEX_URL = reverse('horizon:nova:images_and_snapshots:images:index')
|
||||
|
||||
|
||||
class FakeQuota:
|
||||
@@ -67,77 +67,6 @@ class ImageViewTests(test.BaseViewTests):
|
||||
security_group.name = 'default'
|
||||
self.security_groups = (security_group,)
|
||||
|
||||
def test_index(self):
|
||||
self.mox.StubOutWithMock(api, 'image_list_detailed')
|
||||
api.image_list_detailed(IsA(http.HttpRequest)).AndReturn(self.images)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'tenant_quota_get')
|
||||
api.tenant_quota_get(IsA(http.HttpRequest), self.TEST_TENANT) \
|
||||
.AndReturn({})
|
||||
|
||||
self.mox.StubOutWithMock(api, 'flavor_list')
|
||||
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'keypair_list')
|
||||
api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'security_group_list')
|
||||
api.security_group_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.security_groups)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(IMAGES_INDEX_URL)
|
||||
|
||||
self.assertTemplateUsed(res, 'nova/images/index.html')
|
||||
|
||||
self.assertIn('images', res.context)
|
||||
images = res.context['images']
|
||||
self.assertEqual(len(images), 1)
|
||||
self.assertEqual(images[0].name, 'visibleImage')
|
||||
|
||||
def test_index_no_images(self):
|
||||
self.mox.StubOutWithMock(api, 'image_list_detailed')
|
||||
api.image_list_detailed(IsA(http.HttpRequest)).AndReturn([])
|
||||
|
||||
self.mox.StubOutWithMock(messages, 'info')
|
||||
messages.info(IsA(http.HttpRequest), IsA(basestring))
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(IMAGES_INDEX_URL)
|
||||
|
||||
self.assertTemplateUsed(res, 'nova/images/index.html')
|
||||
|
||||
def test_index_client_conn_error(self):
|
||||
self.mox.StubOutWithMock(api, 'image_list_detailed')
|
||||
exception = glance_exception.ClientConnectionError('clientConnError')
|
||||
api.image_list_detailed(IsA(http.HttpRequest)).AndRaise(exception)
|
||||
|
||||
self.mox.StubOutWithMock(messages, 'error')
|
||||
messages.error(IsA(http.HttpRequest), IsA(basestring))
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(IMAGES_INDEX_URL)
|
||||
|
||||
self.assertTemplateUsed(res,
|
||||
'nova/images/index.html')
|
||||
|
||||
def test_index_glance_error(self):
|
||||
self.mox.StubOutWithMock(api, 'image_list_detailed')
|
||||
exception = glance_exception.GlanceException('glanceError')
|
||||
api.image_list_detailed(IsA(http.HttpRequest)).AndRaise(exception)
|
||||
|
||||
self.mox.StubOutWithMock(messages, 'error')
|
||||
messages.error(IsA(http.HttpRequest), IsA(basestring))
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(IMAGES_INDEX_URL)
|
||||
|
||||
self.assertTemplateUsed(res, 'nova/images/index.html')
|
||||
|
||||
def test_launch_get(self):
|
||||
IMAGE_ID = '1'
|
||||
|
||||
@@ -161,10 +90,12 @@ class ImageViewTests(test.BaseViewTests):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse('horizon:nova:images:launch',
|
||||
args=[IMAGE_ID]))
|
||||
res = self.client.get(
|
||||
reverse('horizon:nova:images_and_snapshots:images:launch',
|
||||
args=[IMAGE_ID]))
|
||||
|
||||
self.assertTemplateUsed(res, 'nova/images/launch.html')
|
||||
self.assertTemplateUsed(res,
|
||||
'nova/images_and_snapshots/images/launch.html')
|
||||
|
||||
image = res.context['image']
|
||||
self.assertEqual(image.name, self.visibleImage.name)
|
||||
@@ -232,9 +163,10 @@ class ImageViewTests(test.BaseViewTests):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.post(reverse('horizon:nova:images:launch',
|
||||
args=[IMAGE_ID]),
|
||||
form_data)
|
||||
res = self.client.post(
|
||||
reverse('horizon:nova:images_and_snapshots:images:launch',
|
||||
args=[IMAGE_ID]),
|
||||
form_data)
|
||||
|
||||
self.assertRedirectsNoFollow(res,
|
||||
reverse('horizon:nova:instances_and_volumes:instances:index'))
|
||||
@@ -263,10 +195,12 @@ class ImageViewTests(test.BaseViewTests):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse('horizon:nova:images:launch',
|
||||
args=[IMAGE_ID]))
|
||||
res = self.client.get(
|
||||
reverse('horizon:nova:images_and_snapshots:images:launch',
|
||||
args=[IMAGE_ID]))
|
||||
|
||||
self.assertTemplateUsed(res, 'nova/images/launch.html')
|
||||
self.assertTemplateUsed(res,
|
||||
'nova/images_and_snapshots/images/launch.html')
|
||||
|
||||
form = res.context['form']
|
||||
|
||||
@@ -297,10 +231,12 @@ class ImageViewTests(test.BaseViewTests):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse('horizon:nova:images:launch',
|
||||
args=[IMAGE_ID]))
|
||||
res = self.client.get(
|
||||
reverse('horizon:nova:images_and_snapshots:images:launch',
|
||||
args=[IMAGE_ID]))
|
||||
|
||||
self.assertTemplateUsed(res, 'nova/images/launch.html')
|
||||
self.assertTemplateUsed(res,
|
||||
'nova/images_and_snapshots/images/launch.html')
|
||||
|
||||
form = res.context['form']
|
||||
|
||||
@@ -362,7 +298,9 @@ class ImageViewTests(test.BaseViewTests):
|
||||
messages.error(IsA(http.HttpRequest), IsA(basestring))
|
||||
|
||||
self.mox.ReplayAll()
|
||||
url = reverse('horizon:nova:images:launch', args=[IMAGE_ID])
|
||||
url = reverse('horizon:nova:images_and_snapshots:images:launch',
|
||||
args=[IMAGE_ID])
|
||||
res = self.client.post(url, form_data)
|
||||
|
||||
self.assertTemplateUsed(res, 'nova/images/launch.html')
|
||||
self.assertTemplateUsed(res,
|
||||
'nova/images_and_snapshots/images/launch.html')
|
||||
@@ -18,10 +18,10 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf.urls.defaults import *
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
|
||||
|
||||
urlpatterns = patterns('horizon.dashboards.nova.images.views',
|
||||
urlpatterns = patterns('horizon.dashboards.nova.images_and_snapshots.images.views',
|
||||
url(r'^$', 'index', name='index'),
|
||||
url(r'^(?P<image_id>[^/]+)/launch/$', 'launch', name='launch'),
|
||||
url(r'^(?P<image_id>[^/]+)/update/$', 'update', name='update'))
|
||||
@@ -33,8 +33,8 @@ from novaclient import exceptions as novaclient_exceptions
|
||||
from openstackx.api import exceptions as api_exceptions
|
||||
|
||||
from horizon import api
|
||||
from horizon.dashboards.nova.images.forms import (UpdateImageForm,
|
||||
LaunchForm, DeleteImage)
|
||||
from horizon.dashboards.nova.images_and_snapshots.images.forms import \
|
||||
(UpdateImageForm, LaunchForm, DeleteImage)
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@@ -67,11 +67,11 @@ def index(request):
|
||||
|
||||
context = {'delete_form': DeleteImage(), 'images': images}
|
||||
|
||||
if images:
|
||||
quotas = api.tenant_quota_get(request, request.user.tenant_id)
|
||||
context['quotas'] = quotas
|
||||
|
||||
return shortcuts.render(request, 'nova/images/index.html', context)
|
||||
return shortcuts.render(request,
|
||||
'nova/images_and_snapshots/images/index.html', {
|
||||
'delete_form': DeleteImage(),
|
||||
'quotas': quotas,
|
||||
'images': images})
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -131,7 +131,7 @@ def launch(request, image_id):
|
||||
return handled
|
||||
|
||||
return shortcuts.render(request,
|
||||
'nova/images/launch.html', {
|
||||
'nova/images_and_snapshots/images/launch.html', {
|
||||
'image': image,
|
||||
'form': form,
|
||||
'quotas': quotas})
|
||||
@@ -164,4 +164,4 @@ def update(request, image_id):
|
||||
|
||||
context = {'form': form, "image": image}
|
||||
|
||||
return shortcuts.render(request, 'nova/images/update.html', context)
|
||||
return shortcuts.render(request, 'nova/images_and_snapshots/images/update.html', context)
|
||||
@@ -0,0 +1,27 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 Nebula, Inc.
|
||||
# Copyright 2011 OpenStack LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import horizon
|
||||
from horizon.dashboards.nova import dashboard
|
||||
|
||||
|
||||
class ImagesAndSnapshots(horizon.Panel):
|
||||
name = "Images & Snapshots"
|
||||
slug = 'images_and_snapshots'
|
||||
|
||||
|
||||
dashboard.Nova.register(ImagesAndSnapshots)
|
||||
@@ -49,7 +49,7 @@ class CreateSnapshot(forms.SelfHandlingForm):
|
||||
messages.info(request,
|
||||
_('Snapshot "%(name)s" created for instance "%(inst)s"') %
|
||||
{"name": data['name'], "inst": instance.name})
|
||||
return shortcuts.redirect('horizon:nova:snapshots:index')
|
||||
return shortcuts.redirect('horizon:nova:images_and_snapshots:snapshots:index')
|
||||
except api_exceptions.ApiException, e:
|
||||
msg = _('Error Creating Snapshot: %s') % e.message
|
||||
LOG.exception(msg)
|
||||
@@ -65,59 +65,6 @@ class SnapshotsViewTests(test.BaseViewTests):
|
||||
security_group.name = 'default'
|
||||
self.security_groups = (security_group,)
|
||||
|
||||
def test_index(self):
|
||||
self.mox.StubOutWithMock(api, 'snapshot_list_detailed')
|
||||
api.snapshot_list_detailed(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.images)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'flavor_list')
|
||||
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'keypair_list')
|
||||
api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'security_group_list')
|
||||
api.security_group_list(IsA(http.HttpRequest)).AndReturn(
|
||||
self.security_groups)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse('horizon:nova:snapshots:index'))
|
||||
|
||||
self.assertTemplateUsed(res, 'nova/snapshots/index.html')
|
||||
|
||||
self.assertIn('images', res.context)
|
||||
images = res.context['images']
|
||||
self.assertEqual(len(images), 1)
|
||||
|
||||
def test_index_client_conn_error(self):
|
||||
self.mox.StubOutWithMock(api, 'snapshot_list_detailed')
|
||||
exception = glance_exception.ClientConnectionError('clientConnError')
|
||||
api.snapshot_list_detailed(IsA(http.HttpRequest)).AndRaise(exception)
|
||||
|
||||
self.mox.StubOutWithMock(messages, 'error')
|
||||
messages.error(IsA(http.HttpRequest), IsA(basestring))
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse('horizon:nova:snapshots:index'))
|
||||
|
||||
self.assertTemplateUsed(res, 'nova/snapshots/index.html')
|
||||
|
||||
def test_index_glance_error(self):
|
||||
self.mox.StubOutWithMock(api, 'snapshot_list_detailed')
|
||||
exception = glance_exception.GlanceException('glanceError')
|
||||
api.snapshot_list_detailed(IsA(http.HttpRequest)).AndRaise(exception)
|
||||
|
||||
self.mox.StubOutWithMock(messages, 'error')
|
||||
messages.error(IsA(http.HttpRequest), IsA(basestring))
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse('horizon:nova:snapshots:index'))
|
||||
|
||||
self.assertTemplateUsed(res, 'nova/snapshots/index.html')
|
||||
|
||||
def test_create_snapshot_get(self):
|
||||
self.mox.StubOutWithMock(api, 'server_get')
|
||||
api.server_get(IsA(http.HttpRequest),
|
||||
@@ -125,10 +72,12 @@ class SnapshotsViewTests(test.BaseViewTests):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse('horizon:nova:snapshots:create',
|
||||
args=[self.good_server.id]))
|
||||
res = self.client.get(
|
||||
reverse('horizon:nova:images_and_snapshots:snapshots:create',
|
||||
args=[self.good_server.id]))
|
||||
|
||||
self.assertTemplateUsed(res, 'nova/snapshots/create.html')
|
||||
self.assertTemplateUsed(res,
|
||||
'nova/images_and_snapshots/snapshots/create.html')
|
||||
|
||||
def test_create_snapshot_get_with_invalid_status(self):
|
||||
self.mox.StubOutWithMock(api, 'server_get')
|
||||
@@ -137,8 +86,9 @@ class SnapshotsViewTests(test.BaseViewTests):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse('horizon:nova:snapshots:create',
|
||||
args=[self.bad_server.id]))
|
||||
res = self.client.get(
|
||||
reverse('horizon:nova:images_and_snapshots:snapshots:create',
|
||||
args=[self.bad_server.id]))
|
||||
|
||||
self.assertRedirectsNoFollow(res,
|
||||
reverse('horizon:nova:instances_and_volumes:instances:index'))
|
||||
@@ -151,8 +101,9 @@ class SnapshotsViewTests(test.BaseViewTests):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse('horizon:nova:snapshots:create',
|
||||
args=[self.good_server.id]))
|
||||
res = self.client.get(
|
||||
reverse('horizon:nova:images_and_snapshots:snapshots:create',
|
||||
args=[self.good_server.id]))
|
||||
|
||||
self.assertRedirectsNoFollow(res,
|
||||
reverse('horizon:nova:instances_and_volumes:instances:index'))
|
||||
@@ -179,12 +130,13 @@ class SnapshotsViewTests(test.BaseViewTests):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.post(reverse('horizon:nova:snapshots:create',
|
||||
args=[self.good_server.id]),
|
||||
formData)
|
||||
res = self.client.post(
|
||||
reverse('horizon:nova:images_and_snapshots:snapshots:create',
|
||||
args=[self.good_server.id]),
|
||||
formData)
|
||||
|
||||
self.assertRedirectsNoFollow(res,
|
||||
reverse('horizon:nova:snapshots:index'))
|
||||
reverse('horizon:nova:images_and_snapshots:snapshots:index'))
|
||||
|
||||
def test_create_snapshot_post_exception(self):
|
||||
SNAPSHOT_NAME = 'snappy'
|
||||
@@ -206,10 +158,11 @@ class SnapshotsViewTests(test.BaseViewTests):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.post(reverse('horizon:nova:snapshots:create',
|
||||
args=[self.good_server.id]),
|
||||
formData)
|
||||
res = self.client.post(
|
||||
reverse('horizon:nova:images_and_snapshots:snapshots:create',
|
||||
args=[self.good_server.id]),
|
||||
formData)
|
||||
|
||||
self.assertRedirectsNoFollow(res,
|
||||
reverse('horizon:nova:snapshots:create',
|
||||
args=[self.good_server.id]))
|
||||
reverse('horizon:nova:images_and_snapshots:snapshots:create',
|
||||
args=[self.good_server.id]))
|
||||
@@ -21,6 +21,6 @@
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
|
||||
|
||||
urlpatterns = patterns('horizon.dashboards.nova.snapshots.views',
|
||||
urlpatterns = patterns('horizon.dashboards.nova.images_and_snapshots.snapshots.views',
|
||||
url(r'^$', 'index', name='index'),
|
||||
url(r'^(?P<instance_id>[^/]+)/create', 'create', name='create'))
|
||||
@@ -36,7 +36,8 @@ from openstackx.api import exceptions as api_exceptions
|
||||
|
||||
from horizon import api
|
||||
from horizon import forms
|
||||
from horizon.dashboards.nova.snapshots.forms import CreateSnapshot
|
||||
from horizon.dashboards.nova.images_and_snapshots.snapshots.forms import \
|
||||
CreateSnapshot
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@@ -58,7 +59,7 @@ def index(request):
|
||||
messages.error(request, msg)
|
||||
|
||||
return shortcuts.render(request,
|
||||
'nova/snapshots/index.html',
|
||||
'nova/images_and_snapshots/snapshots/index.html',
|
||||
{'images': images})
|
||||
|
||||
|
||||
@@ -89,6 +90,6 @@ def create(request, instance_id):
|
||||
'horizon:nova:instances_and_volumes:instances:index')
|
||||
|
||||
return shortcuts.render(request,
|
||||
'nova/snapshots/create.html',
|
||||
'nova/images_and_snapshots/snapshots/create.html',
|
||||
{'instance': instance,
|
||||
'create_form': form})
|
||||
157
horizon/horizon/dashboards/nova/images_and_snapshots/tests.py
Normal file
157
horizon/horizon/dashboards/nova/images_and_snapshots/tests.py
Normal file
@@ -0,0 +1,157 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Copyright 2011 Nebula, Inc.
|
||||
# Copyright 2011 OpenStack LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django import http
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from glance.common import exception as glance_exception
|
||||
from mox import IsA
|
||||
|
||||
from horizon import api
|
||||
from horizon import test
|
||||
|
||||
|
||||
INDEX_URL = reverse('horizon:nova:images_and_snapshots:index')
|
||||
|
||||
|
||||
class ImagesAndSnapshotsTests(test.BaseViewTests):
|
||||
def setUp(self):
|
||||
super(ImagesAndSnapshotsTests, self).setUp()
|
||||
snapshot_dict = {'name': 'snapshot',
|
||||
'container_format': 'ami',
|
||||
'id': 3}
|
||||
snapshot = api.Image(snapshot_dict)
|
||||
self.snapshots = [snapshot, ]
|
||||
|
||||
image_dict = {'name': 'visibleImage',
|
||||
'container_format': 'novaImage'}
|
||||
self.visibleImage = api.Image(image_dict)
|
||||
self.visibleImage.id = '1'
|
||||
|
||||
image_dict = {'name': 'invisibleImage',
|
||||
'container_format': 'aki'}
|
||||
self.invisibleImage = api.Image(image_dict)
|
||||
self.invisibleImage.id = '2'
|
||||
|
||||
flavor = api.Flavor(None)
|
||||
flavor.id = 1
|
||||
flavor.name = 'm1.massive'
|
||||
flavor.vcpus = 1000
|
||||
flavor.disk = 1024
|
||||
flavor.ram = 10000
|
||||
self.flavors = (flavor,)
|
||||
|
||||
self.images = (self.visibleImage, self.invisibleImage)
|
||||
|
||||
keypair = api.KeyPair(None)
|
||||
keypair.name = 'keyName'
|
||||
self.keypairs = (keypair,)
|
||||
|
||||
security_group = api.SecurityGroup(None)
|
||||
security_group.name = 'default'
|
||||
self.security_groups = (security_group,)
|
||||
|
||||
def test_index(self):
|
||||
self.mox.StubOutWithMock(api, 'image_list_detailed')
|
||||
api.image_list_detailed(IsA(http.HttpRequest)).AndReturn(self.images)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'snapshot_list_detailed')
|
||||
api.snapshot_list_detailed(IsA(http.HttpRequest)).AndReturn(
|
||||
self.snapshots)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'flavor_list')
|
||||
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors)
|
||||
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'security_group_list')
|
||||
api.security_group_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.security_groups)
|
||||
api.security_group_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.security_groups)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'keypair_list')
|
||||
api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'tenant_quota_get')
|
||||
api.tenant_quota_get(IsA(http.HttpRequest),
|
||||
self.TEST_TENANT).AndReturn({})
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(INDEX_URL)
|
||||
|
||||
self.assertTemplateUsed(res,
|
||||
'nova/images_and_snapshots/index.html')
|
||||
|
||||
self.assertIn('images', res.context)
|
||||
images = res.context['images']
|
||||
self.assertEqual(len(images), 1)
|
||||
self.assertEqual(images[0].name, 'visibleImage')
|
||||
|
||||
def test_index_no_images(self):
|
||||
self.mox.StubOutWithMock(api, 'snapshot_list_detailed')
|
||||
api.snapshot_list_detailed(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.snapshots)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'flavor_list')
|
||||
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'security_group_list')
|
||||
api.security_group_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(self.security_groups)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'keypair_list')
|
||||
api.keypair_list(IsA(http.HttpRequest)).AndReturn(self.keypairs)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'image_list_detailed')
|
||||
api.image_list_detailed(IsA(http.HttpRequest)).AndReturn([])
|
||||
|
||||
self.mox.StubOutWithMock(api, 'tenant_quota_get')
|
||||
api.tenant_quota_get(IsA(http.HttpRequest), self.TEST_TENANT) \
|
||||
.AndReturn({})
|
||||
|
||||
self.mox.StubOutWithMock(messages, 'info')
|
||||
messages.info(IsA(http.HttpRequest), IsA(basestring))
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(INDEX_URL)
|
||||
|
||||
self.assertTemplateUsed(res, 'nova/images_and_snapshots/index.html')
|
||||
|
||||
def test_index_client_conn_error(self):
|
||||
|
||||
self.mox.StubOutWithMock(api, 'image_list_detailed')
|
||||
exception = glance_exception.ClientConnectionError('clientConnError')
|
||||
api.image_list_detailed(IsA(http.HttpRequest)).AndRaise(exception)
|
||||
|
||||
self.mox.StubOutWithMock(api, 'tenant_quota_get')
|
||||
api.tenant_quota_get(IsA(http.HttpRequest), self.TEST_TENANT) \
|
||||
.AndReturn({})
|
||||
|
||||
self.mox.StubOutWithMock(messages, 'error')
|
||||
messages.error(IsA(http.HttpRequest), IsA(basestring))
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(INDEX_URL)
|
||||
|
||||
self.assertTemplateUsed(res, 'nova/images_and_snapshots/index.html')
|
||||
34
horizon/horizon/dashboards/nova/images_and_snapshots/urls.py
Normal file
34
horizon/horizon/dashboards/nova/images_and_snapshots/urls.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Copyright 2011 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.defaults import *
|
||||
|
||||
import horizon
|
||||
|
||||
from horizon.dashboards.nova.images_and_snapshots.images import urls\
|
||||
as image_urls
|
||||
from horizon.dashboards.nova.images_and_snapshots.snapshots import urls\
|
||||
as snapshot_urls
|
||||
|
||||
urlpatterns = patterns('horizon.dashboards.nova.images_and_snapshots',
|
||||
url(r'^$', 'views.index', name='index'),
|
||||
url(r'', include(image_urls, namespace='images')),
|
||||
url(r'', include(snapshot_urls, namespace='snapshots')),
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Copyright 2011 Nebula, Inc.
|
||||
# Copyright 2011 Openstack LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Views for managing Images and Snapshots.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django import shortcuts
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.translation import ugettext as _
|
||||
from glance.common import exception as glance_exception
|
||||
from novaclient import exceptions as novaclient_exceptions
|
||||
from openstackx.api import exceptions as api_exceptions
|
||||
|
||||
from horizon import api
|
||||
from horizon.dashboards.nova.images_and_snapshots.images.forms import \
|
||||
(UpdateImageForm, LaunchForm, DeleteImage)
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@login_required
|
||||
def index(request):
|
||||
for f in (DeleteImage, ):
|
||||
unused, handled = f.maybe_handle(request)
|
||||
if handled:
|
||||
return handled
|
||||
all_images = []
|
||||
snapshots = []
|
||||
try:
|
||||
all_images = api.image_list_detailed(request)
|
||||
snapshots = api.snapshot_list_detailed(request)
|
||||
if not all_images:
|
||||
messages.info(request, _("There are currently no images."))
|
||||
except glance_exception.ClientConnectionError, e:
|
||||
LOG.exception("Error connecting to glance")
|
||||
messages.error(request, _("Error connecting to glance: %s") % str(e))
|
||||
except glance_exception.Error, e:
|
||||
LOG.exception("Error retrieving image list")
|
||||
messages.error(request, _("Unable to fetch images: %s") % str(e))
|
||||
except api_exceptions.ApiException, e:
|
||||
msg = _("Unable to retrieve image info from glance: %s") % str(e)
|
||||
LOG.exception(msg)
|
||||
messages.error(request, msg)
|
||||
images = [im for im in all_images
|
||||
if im['container_format'] not in ['aki', 'ari']]
|
||||
|
||||
quotas = api.tenant_quota_get(request, request.user.tenant_id)
|
||||
|
||||
return shortcuts.render(request,
|
||||
'nova/images_and_snapshots/index.html', {
|
||||
'delete_form': DeleteImage(),
|
||||
'quotas': quotas,
|
||||
'images': images,
|
||||
'snapshots': snapshots})
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_header %}
|
||||
{% url horizon:nova:images:index as refresh_link %}
|
||||
{% url horizon:nova:images_and_snapshots:images:index as refresh_link %}
|
||||
{# to make searchable false, just remove it from the include statement #}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Containers") refresh_link=refresh_link searchable="true" %}
|
||||
{% endblock page_header %}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block form_id %}{% endblock %}
|
||||
{% block form_action %}{% url horizon:nova:images:launch image_id %}{% endblock %}
|
||||
{% block form_action %}{% url horizon:nova:images_and_snapshots:images:launch image_id %}{% endblock %}
|
||||
|
||||
{% block modal_id %}launch_image_{{ image_id }}{% endblock %}
|
||||
|
||||
@@ -48,5 +48,5 @@
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn primary pull-right" type="submit" value="{% trans "Launch Instance" %}" />
|
||||
<a href="{% url horizon:nova:images:index %}" class="btn secondary cancel close">Cancel</a>
|
||||
<a href="{% url horizon:nova:images_and_snapshots:images:index %}" class="btn secondary cancel close">Cancel</a>
|
||||
{% endblock %}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
<div class="table_title">
|
||||
<h3>Images and Snapshots</h3>
|
||||
<h3>Images</h3>
|
||||
<div class="table_actions">
|
||||
<div class="images table_search">
|
||||
<form action="#">
|
||||
@@ -32,14 +32,14 @@
|
||||
<td>{{image.properties.image_type|default:"Image"}}</td>
|
||||
<td>{{image.status|capfirst}}</td>
|
||||
<td>
|
||||
<a class="btn small primary" data-controls-modal="launch_image_{{image.id}}" data-backdrop="static" href="{% url horizon:nova:images:launch image.id %}">{% trans "Launch" %}</a>
|
||||
<a class="btn small primary" data-controls-modal="launch_image_{{image.id}}" data-backdrop="static" href="{% url horizon:nova:images_and_snapshots:images:launch image.id %}">{% trans "Launch" %}</a>
|
||||
</td>
|
||||
<td id="name_{{image.name}}" class="actions">
|
||||
{% if image.owner == request.user.tenant_id %}
|
||||
<a class="more-actions" href="#">More</a>
|
||||
<ul>
|
||||
<li><a class='btn small' href="{% url horizon:nova:images:update image.id %}">{% trans "Edit" %}</a></li>
|
||||
<li class="form">{% include "nova/images/_delete.html" with form=delete_form %}</li>
|
||||
<li><a class='btn small' href="{% url horizon:nova:images_and_snapshots:images:update image.id %}">{% trans "Edit" %}</a></li>
|
||||
<li class="form">{% include "nova/images_and_snapshots/images/_delete.html" with form=delete_form %}</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</td>
|
||||
@@ -49,5 +49,5 @@
|
||||
|
||||
{% for image in images %}
|
||||
{% launch_form request request.user.tenant_id image.id as launch_form %}
|
||||
{% include 'nova/images/_launch.html' with form=launch_form image_id=image.id hide=True %}
|
||||
{% include 'nova/images_and_snapshots/images/_launch.html' with form=launch_form image_id=image.id hide=True %}
|
||||
{% endfor %}
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block form_id %}update_image_form{% endblock %}
|
||||
{% block form_action %}{% url horizon:nova:images:update image.id %}{% endblock %}
|
||||
{% block form_action %}{% url horizon:nova:images_and_snapshots:images:update image.id %}{% endblock %}
|
||||
|
||||
{% block modal-header %}Update Image{% endblock %}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn primary pull-right" type="submit" value="{%trans "Update Image"%}" />
|
||||
<a href="{% url horizon:nova:images:index %}" class="btn secondary cancel close">Cancel</a>
|
||||
<a href="{% url horizon:nova:images_and_snapshots:images:index %}" class="btn secondary cancel close">Cancel</a>
|
||||
{% endblock %}
|
||||
@@ -2,14 +2,14 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_header %}
|
||||
{% url horizon:nova:images:index as refresh_link %}
|
||||
{% url horizon:nova:images_and_snapshots:images:index as refresh_link %}
|
||||
{# to make searchable false, just remove it from the include statement #}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Images") refresh_link=refresh_link searchable="true" %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block dash_main %}
|
||||
{% if images %}
|
||||
{% include 'nova/images/_list.html' %}
|
||||
{% include 'nova/images_and_snapshots/images/_list.html' %}
|
||||
{% else %}
|
||||
<div class="alert-message block-message info">
|
||||
<p><strong>{% trans "Info: "%}</strong>{% trans "There are currently no images."%}</p>
|
||||
@@ -13,6 +13,6 @@
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block dash_main %}
|
||||
{% include 'nova/images/_launch.html' with image_id=image.id %}
|
||||
{% include 'nova/images_and_snapshots/images/_launch.html' with image_id=image.id %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block dash_main %}
|
||||
{% include 'nova/images/_update.html' %}
|
||||
{% include 'nova/images_and_snapshots/images/_update.html' %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends 'nova/base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Images & Snapshots") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block dash_main %}
|
||||
{% if images %}
|
||||
{% include 'nova/images_and_snapshots/images/_list.html' %}
|
||||
{% else %}
|
||||
<div class="alert-message block-message info">
|
||||
<p><strong>{% trans "Info: "%}</strong>{% trans "There are currently no images."%}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if snapshots %}
|
||||
{% include 'nova/images_and_snapshots/snapshots/_list.html' %}
|
||||
{% else %}
|
||||
<div class="alert-message block-message info">
|
||||
<p><strong>{% trans "Info: "%}</strong>{% trans "There are currently no snapshots. You can create snapshots from running instances."%}</p>
|
||||
<div class="alert-actions">
|
||||
<a class="btn small primary" href='{% url horizon:nova:instances_and_volumes:instances:index %}'>{% trans "View Running Instances" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -2,7 +2,7 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block form_id %}create_snapshot_form{% endblock %}
|
||||
{% block form_action %}{% url horizon:nova:snapshots:create instance.id %}{% endblock %}
|
||||
{% block form_action %}{% url horizon:nova:images_and_snapshots:snapshots:create instance.id %}{% endblock %}
|
||||
|
||||
{% block modal_id %}create_snapshot_modal{% endblock %}
|
||||
{% block modal-header %}{%trans "Create Snapshot"%}{% endblock %}
|
||||
@@ -21,5 +21,5 @@
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn primary pull-right" type="submit" value="{%trans "Create Snapshot"%}" />
|
||||
<a href="{% url horizon:nova:snapshots:index %}" class="btn secondary cancel close">Cancel</a>
|
||||
<a href="{% url horizon:nova:images_and_snapshots:snapshots:index %}" class="btn secondary cancel close">Cancel</a>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,53 @@
|
||||
{% load i18n parse_date launch_form %}
|
||||
|
||||
|
||||
<div class="table_title">
|
||||
<h3>Snapshots</h3>
|
||||
<div class="table_actions">
|
||||
<div class="images table_search">
|
||||
<form action="#">
|
||||
<input class="span3" type="text">
|
||||
</form>
|
||||
</div>
|
||||
<a class="inspect" href="#">{% trans "inspect" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="images" class="zebra-striped sortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th colspan="2">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for image in snapshots %}
|
||||
<tr class="{% cycle 'odd' 'even' %}">
|
||||
<td class="select">
|
||||
<input type="checkbox" name="image_{{image.id}}" value="image_{{image.id}}" id="image_select_{{image.id}}" />
|
||||
</td>
|
||||
<td>{{image.name}}</td>
|
||||
<td>{{image.properties.image_type|default:"Image"}}</td>
|
||||
<td>{{image.status|capfirst}}</td>
|
||||
<td>
|
||||
<a class="btn small primary" data-controls-modal="launch_image_{{image.id}}" data-backdrop="static" href="{% url horizon:nova:images_and_snapshots:images:launch image.id %}">{% trans "Launch" %}</a>
|
||||
</td>
|
||||
<td id="name_{{image.name}}" class="actions">
|
||||
{% if image.owner == request.user.tenant_id %}
|
||||
<a class="more-actions" href="#">View</a>
|
||||
<ul>
|
||||
<li><a class='btn small' href="{% url horizon:nova:images_and_snapshots:images:update image.id %}">{% trans "Edit" %}</a></li>
|
||||
<li class="form">{% include "nova/images_and_snapshots/images/_delete.html" with form=delete_form %}</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% for image in snapshots %}
|
||||
{% launch_form request request.user.tenant_id image.id as launch_form %}
|
||||
{% include 'nova/images_and_snapshots/images/_launch.html' with form=launch_form image_id=image.id hide=True %}
|
||||
{% endfor %}
|
||||
@@ -6,7 +6,7 @@
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block dash_main %}
|
||||
{% include 'nova/snapshots/_create.html' with form=create_form %}
|
||||
{% include 'nova/images_and_snapshots/snapshots/_create.html' with form=create_form %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
{% block dash_main %}
|
||||
{% if images %}
|
||||
{% include 'nova/images/_list.html' %}
|
||||
{% include 'nova/images_and_snapshots/images/_list.html' %}
|
||||
{% else %}
|
||||
<div class="alert-message block-message info">
|
||||
<p><strong>{% trans "Info: "%}</strong>{% trans "There are currently no snapshots. You can create snapshots from running instances."%}</p>
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="table_title">
|
||||
<h3>{% trans "My Instances" %}</h3>
|
||||
<div class="table_actions">
|
||||
<a class="btn small primary" href='{% url horizon:nova:images:index %}'>{% trans "Launch Instance" %}</a>
|
||||
<a class="btn small primary" href='{% url horizon:nova:images_and_snapshots:images:index %}'>{% trans "Launch Instance" %}</a>
|
||||
<div class="instances table_search">
|
||||
<form action="#">
|
||||
<input class="span3" type="text">
|
||||
@@ -44,7 +44,7 @@
|
||||
<li><a class="btn small" target='_blank' href='{% url horizon:nova:instances_and_volumes:instances:vnc instance.id %}'>{% trans 'VNC Console'%}</a></li>
|
||||
<li><a class='btn small' target='_blank' href='{% url horizon:nova:instances_and_volumes:instances:console instance.id %}'>{% trans 'Log'%}</a></li>
|
||||
<li><a class='btn small' href='{% url horizon:nova:instances_and_volumes:instances:update instance.id %}'>{% trans 'Edit'%}</a></li>
|
||||
<li><a class='btn small' href='{% url horizon:nova:snapshots:create instance.id %}'>{% trans 'Snapshot'%}</a></li>
|
||||
<li><a class='btn small' href='{% url horizon:nova:images_and_snapshots:snapshots:create instance.id %}'>{% trans 'Snapshot'%}</a></li>
|
||||
<li>{% include 'nova/instances_and_volumes/instances/_reboot.html' with form=reboot_form %}</li>
|
||||
<li>{% include 'nova/instances_and_volumes/instances/_terminate.html' with form=terminate_form %}</li>
|
||||
</ul>
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
<div class="alert-message block-message info">
|
||||
<p><strong>{% trans "Info: " %}</strong>{% trans "There are no instances running. Launch an instance from the Images Page." %}</p>
|
||||
<div class="alert-actions">
|
||||
<a class="btn small primary" href='{% url horizon:nova:images:index %}'>{% trans "Images Page" %}</a>
|
||||
<a class="btn small primary" href='{% url horizon:nova:images_and_snapshots:images:index %}'>{% trans "Images Page" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends 'nova/instances_and_volumes/instances/_form.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block form_action %}{% url horizon:nova:snapshots:create instance.id %}{% endblock %}
|
||||
{% block form_action %}{% url horizon:nova:images_and_snapshots:snapshots:create instance.id %}{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<div class="modal-footer">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{% include 'syspanel/instances/_list.html' %}
|
||||
{% else %}
|
||||
<div class="alert-message block-message info">
|
||||
{% url horizon:nova:images:index as dash_image_url%}
|
||||
{% url horizon:nova:images_and_snapshots:images:index as dash_image_url%}
|
||||
<p><strong>{% trans "Info: "%}</strong>{% blocktrans %}There are currently no instances. You can launch an instance from the <a class="btn small" href='{{dash_image_url}}'>Images Page.</a>{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -28,9 +28,10 @@ from django import template
|
||||
from openstackx.api import exceptions as api_exceptions
|
||||
from novaclient import exceptions as novaclient_exceptions
|
||||
from horizon import api
|
||||
from horizon.dashboards.nova.images.forms import LaunchForm
|
||||
from horizon.utils import assignment_tag
|
||||
from horizon.dashboards.nova.images_and_snapshots.images.forms import \
|
||||
LaunchForm
|
||||
|
||||
from horizon.utils import assignment_tag
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
register = template.Library()
|
||||
|
||||
Reference in New Issue
Block a user