Volumes Redux.

Makes all volume-service interactions optional (includes
volumes displays and volumes portion of launch workflow).

Splits Instances & Volumes into separate panels to
facilitate Volumes being optional.

Adds an admin Volumes panel to complement the user
Volumes panel.

Adds the capability to mark Steps in a Workflow as
requiring a given service.

Implements blueprint nova-volume-optional.

Change-Id: I3fb4255617ae52be64e7a300afd691dc36b95586
This commit is contained in:
Gabriel Hurley 2012-06-26 13:32:40 -07:00
parent 010475bbcd
commit 2c0a8f3220
65 changed files with 475 additions and 462 deletions

View File

@ -104,7 +104,7 @@ def get_instance_info(instance):
def get_instance_link(datum):
view = "horizon:nova:instances_and_volumes:instances:detail"
view = "horizon:nova:instances:detail"
if datum.instance_id:
return urlresolvers.reverse(view, args=(datum.instance_id,))
else:

View File

@ -91,7 +91,7 @@ class FloatingIpViewTests(test.TestCase):
form_data = {'instance_id': server.id,
'ip_id': floating_ip.id}
url = reverse('%s:associate' % NAMESPACE)
next = reverse("horizon:nova:instances_and_volumes:index")
next = reverse("horizon:nova:instances:index")
res = self.client.post("%s?next=%s" % (url, next), form_data)
self.assertRedirectsNoFollow(res, next)

View File

@ -23,7 +23,8 @@ class BasePanels(horizon.PanelGroup):
slug = "compute"
name = _("Manage Compute")
panels = ('overview',
'instances_and_volumes',
'instances',
'volumes',
'images_and_snapshots',
'access_and_security')

View File

@ -31,7 +31,7 @@ LOG = logging.getLogger(__name__)
class LaunchImage(tables.LinkAction):
name = "launch_image"
verbose_name = _("Launch")
url = "horizon:nova:instances_and_volumes:instances:launch"
url = "horizon:nova:instances:launch"
classes = ("btn-launch", "ajax-modal")
def get_link_url(self, datum):

View File

@ -51,7 +51,7 @@ class CreateSnapshot(forms.SelfHandlingForm):
return shortcuts.redirect('horizon:nova:images_and_snapshots:'
'index')
except:
redirect = reverse("horizon:nova:instances_and_volumes:index")
redirect = reverse("horizon:nova:instances:index")
exceptions.handle(request,
_('Unable to create snapshot.'),
redirect=redirect)

View File

@ -30,7 +30,7 @@ LOG = logging.getLogger(__name__)
class LaunchSnapshot(tables.LinkAction):
name = "launch_snapshot"
verbose_name = _("Launch")
url = "horizon:nova:instances_and_volumes:instances:launch"
url = "horizon:nova:instances:launch"
classes = ("btn-launch", "ajax-modal")
def get_link_url(self, datum):

View File

@ -51,7 +51,7 @@ class SnapshotsViewTests(test.TestCase):
url = reverse('horizon:nova:images_and_snapshots:snapshots:create',
args=[server.id])
res = self.client.get(url)
redirect = reverse("horizon:nova:instances_and_volumes:index")
redirect = reverse("horizon:nova:instances:index")
self.assertRedirectsNoFollow(res, redirect)
def test_create_get_server_exception(self):
@ -64,7 +64,7 @@ class SnapshotsViewTests(test.TestCase):
url = reverse('horizon:nova:images_and_snapshots:snapshots:create',
args=[server.id])
res = self.client.get(url)
redirect = reverse("horizon:nova:instances_and_volumes:index")
redirect = reverse("horizon:nova:instances:index")
self.assertRedirectsNoFollow(res, redirect)
def test_create_snapshot_post(self):
@ -107,5 +107,5 @@ class SnapshotsViewTests(test.TestCase):
url = reverse('horizon:nova:images_and_snapshots:snapshots:create',
args=[server.id])
res = self.client.post(url, formData)
redirect = reverse("horizon:nova:instances_and_volumes:index")
redirect = reverse("horizon:nova:instances:index")
self.assertRedirectsNoFollow(res, redirect)

View File

@ -41,7 +41,7 @@ class CreateView(forms.ModalFormView):
template_name = 'nova/images_and_snapshots/snapshots/create.html'
def get_initial(self):
redirect = reverse('horizon:nova:instances_and_volumes:index')
redirect = reverse('horizon:nova:instances:index')
instance_id = self.kwargs["instance_id"]
try:
self.instance = api.server_get(self.request, instance_id)

View File

@ -20,7 +20,7 @@ from django.utils.translation import ugettext_lazy as _
from horizon import api
from horizon import tables
from ...instances_and_volumes.volumes import tables as volume_tables
from ...volumes import tables as volume_tables
LOG = logging.getLogger(__name__)

View File

@ -32,12 +32,10 @@ INDEX_URL = reverse('horizon:nova:images_and_snapshots:index')
class VolumeSnapshotsViewTests(test.TestCase):
def test_create_snapshot_get(self):
volume = self.volumes.first()
res = self.client.get(reverse('horizon:nova:instances_and_volumes:'
'volumes:create_snapshot',
res = self.client.get(reverse('horizon:nova:volumes:create_snapshot',
args=[volume.id]))
self.assertTemplateUsed(res, 'nova/instances_and_volumes/'
'volumes/create_snapshot.html')
self.assertTemplateUsed(res, 'nova/volumes/create_snapshot.html')
def test_create_snapshot_post(self):
volume = self.volumes.first()
@ -56,7 +54,6 @@ class VolumeSnapshotsViewTests(test.TestCase):
'volume_id': volume.id,
'name': snapshot.display_name,
'description': snapshot.display_description}
url = reverse('horizon:nova:instances_and_volumes:volumes:'
'create_snapshot', args=[volume.id])
url = reverse('horizon:nova:volumes:create_snapshot', args=[volume.id])
res = self.client.post(url, formData)
self.assertRedirectsNoFollow(res, INDEX_URL)

View File

@ -45,5 +45,4 @@ class UpdateInstance(forms.SelfHandlingForm):
except:
exceptions.handle(request, _('Unable to update instance.'))
return shortcuts.redirect(
'horizon:nova:instances_and_volumes:index')
return shortcuts.redirect('horizon:nova:instances:index')

View File

@ -20,9 +20,9 @@ import horizon
from horizon.dashboards.nova import dashboard
class InstancesAndVolumes(horizon.Panel):
name = _("Instances & Volumes")
slug = 'instances_and_volumes'
class Instances(horizon.Panel):
name = _("Instances")
slug = 'instances'
dashboard.Nova.register(InstancesAndVolumes)
dashboard.Nova.register(Instances)

View File

@ -146,14 +146,14 @@ class ToggleSuspend(tables.BatchAction):
class LaunchLink(tables.LinkAction):
name = "launch"
verbose_name = _("Launch Instance")
url = "horizon:nova:instances_and_volumes:instances:launch"
url = "horizon:nova:instances:launch"
classes = ("btn-launch", "ajax-modal")
class EditInstance(tables.LinkAction):
name = "edit"
verbose_name = _("Edit Instance")
url = "horizon:nova:instances_and_volumes:instances:update"
url = "horizon:nova:instances:update"
classes = ("ajax-modal", "btn-edit")
@ -170,7 +170,7 @@ class SnapshotLink(tables.LinkAction):
class ConsoleLink(tables.LinkAction):
name = "console"
verbose_name = _("VNC Console")
url = "horizon:nova:instances_and_volumes:instances:detail"
url = "horizon:nova:instances:detail"
classes = ("btn-console",)
def allowed(self, request, instance=None):
@ -185,7 +185,7 @@ class ConsoleLink(tables.LinkAction):
class LogLink(tables.LinkAction):
name = "log"
verbose_name = _("View Log")
url = "horizon:nova:instances_and_volumes:instances:detail"
url = "horizon:nova:instances:detail"
classes = ("btn-log",)
def allowed(self, request, instance=None):
@ -205,7 +205,7 @@ class AssociateIP(tables.LinkAction):
def get_link_url(self, datum):
base_url = urlresolvers.reverse(self.url)
next = urlresolvers.reverse("horizon:nova:instances_and_volumes:index")
next = urlresolvers.reverse("horizon:nova:instances:index")
params = {"instance_id": self.table.get_object_id(datum),
IPAssociationWorkflow.redirect_param_name: next}
params = urlencode(params)
@ -222,7 +222,7 @@ class UpdateRow(tables.Row):
def get_ips(instance):
template_name = 'nova/instances_and_volumes/instances/_instance_ips.html'
template_name = 'nova/instances/_instance_ips.html'
context = {"instance": instance}
return template.loader.render_to_string(template_name, context)
@ -265,8 +265,7 @@ class InstancesTable(tables.DataTable):
("image_snapshot", "Snapshotting"),
)
name = tables.Column("name",
link=("horizon:nova:instances_and_volumes:"
"instances:detail"),
link=("horizon:nova:instances:detail"),
verbose_name=_("Instance Name"))
ip = tables.Column(get_ips, verbose_name=_("IP Address"))
size = tables.Column(get_size, verbose_name=_("Size"))

View File

@ -24,7 +24,7 @@ from horizon import tabs
class OverviewTab(tabs.Tab):
name = _("Overview")
slug = "overview"
template_name = ("nova/instances_and_volumes/instances/"
template_name = ("nova/instances/"
"_detail_overview.html")
def get_context_data(self, request):
@ -34,7 +34,7 @@ class OverviewTab(tabs.Tab):
class LogTab(tabs.Tab):
name = _("Log")
slug = "log"
template_name = "nova/instances_and_volumes/instances/_detail_log.html"
template_name = "nova/instances/_detail_log.html"
preload = False
def get_context_data(self, request):
@ -53,7 +53,7 @@ class LogTab(tabs.Tab):
class VNCTab(tabs.Tab):
name = _("VNC")
slug = "vnc"
template_name = "nova/instances_and_volumes/instances/_detail_vnc.html"
template_name = "nova/instances/_detail_vnc.html"
preload = False
def get_context_data(self, request):

View File

@ -3,11 +3,11 @@
<div class="clearfix">
<h3 class="pull-left">Instance Console Log</h3>
<p class="pull-right">
{% url horizon:nova:instances_and_volumes:instances:console instance.id as console_url %}
{% url horizon:nova:instances:console instance.id as console_url %}
<a class="btn btn-small" target="_blank" href="{{ console_url }}">{% trans "View Full Log" %}</a>
</p>
<form id="tail_length" action="{% url horizon:nova:instances_and_volumes:instances:console instance.id %}" class="span3 pull-right">
<form id="tail_length" action="{% url horizon:nova:instances:console instance.id %}" class="span3 pull-right">
<label class="pull-left log-length" for="tail_length_select">Log Length</label>
<input class="span1" type="text" name="length" value="35" />
<input value="Go" class="btn-small btn-primary" type="submit" />

View File

@ -87,7 +87,7 @@
{% for volume in instance.volumes %}
<dt>{% trans "Attached To" %}</dt>
<dd>
<a href="{% url horizon:nova:instances_and_volumes:volumes:detail volume.volumeId %}">{{ volume.name }}</a><span> {% trans "on" %} {{ volume.device }}</span>
<a href="{% url horizon:nova:volumes:detail volume.volumeId %}">{{ volume.name }}</a><span> {% trans "on" %} {{ volume.device }}</span>
</dd>
{% empty %}
<dt>{% trans "Volume" %}</dt>

View File

@ -6,5 +6,5 @@
<iframe src="{{ vnc_url }}" width="728" height="436"></iframe>
{% else %}
<p class='alert alert-error'>{% blocktrans %}VNC console is currently unavailabe. Please try again later.{% endblocktrans %}
<a class='btn btn-mini' href="{% url horizon:nova:instances_and_volumes:instances:detail instance_id %}">{% trans "Reload" %}</a></p>
<a class='btn btn-mini' href="{% url horizon:nova:instances:detail instance_id %}">{% trans "Reload" %}</a></p>
{% endif %}

View File

@ -2,7 +2,7 @@
{% load i18n %}
{% block form_id %}update_instance_form{% endblock %}
{% block form_action %}{% url horizon:nova:instances_and_volumes:instances:update instance.id %}{% endblock %}
{% block form_action %}{% url horizon:nova:instances:update instance.id %}{% endblock %}
{% block modal-header %}{% trans "Edit Instance" %}{% endblock %}
@ -20,5 +20,5 @@
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Save Changes" %}" />
<a href="{% url horizon:nova:instances_and_volumes:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url horizon:nova:instances:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

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

View File

@ -7,5 +7,5 @@
{% endblock page_header %}
{% block dash_main %}
{% include 'nova/instances_and_volumes/instances/_update.html' %}
{% include 'nova/instances/_update.html' %}
{% endblock %}

View File

@ -21,6 +21,7 @@
from django import http
from django.core.urlresolvers import reverse
from django.utils.http import urlencode
from django.utils.datastructures import SortedDict
from mox import IsA, IgnoreArg
from copy import deepcopy
@ -31,18 +32,90 @@ from .tabs import InstanceDetailTabs
from .workflows import LaunchInstance
INDEX_URL = reverse('horizon:nova:instances_and_volumes:index')
INDEX_URL = reverse('horizon:nova:instances:index')
class InstanceViewTests(test.TestCase):
class InstanceTests(test.TestCase):
@test.create_stubs({api: ('flavor_list', 'server_list',)})
def test_index(self):
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
self.mox.ReplayAll()
res = self.client.get(
reverse('horizon:nova:instances:index'))
self.assertTemplateUsed(res,
'nova/instances/index.html')
instances = res.context['instances_table'].data
self.assertItemsEqual(instances, self.servers.list())
@test.create_stubs({api: ('server_list',)})
def test_index_server_list_exception(self):
api.server_list(IsA(http.HttpRequest)).AndRaise(self.exceptions.nova)
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:nova:instances:index'))
self.assertTemplateUsed(res, 'nova/instances/index.html')
self.assertEqual(len(res.context['instances_table'].data), 0)
self.assertMessageCount(res, error=1)
@test.create_stubs({api: ('flavor_list', 'server_list', 'flavor_get',)})
def test_index_flavor_list_exception(self):
servers = self.servers.list()
flavors = self.flavors.list()
full_flavors = SortedDict([(f.id, f) for f in flavors])
api.server_list(IsA(http.HttpRequest)).AndReturn(servers)
api.flavor_list(IsA(http.HttpRequest)).AndRaise(self.exceptions.nova)
for server in servers:
api.flavor_get(IsA(http.HttpRequest), server.flavor["id"]). \
AndReturn(full_flavors[server.flavor["id"]])
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:nova:instances:index'))
self.assertTemplateUsed(res, 'nova/instances/index.html')
instances = res.context['instances_table'].data
self.assertItemsEqual(instances, self.servers.list())
@test.create_stubs({api: ('flavor_list', 'server_list', 'flavor_get',)})
def test_index_flavor_get_exception(self):
servers = self.servers.list()
flavors = self.flavors.list()
max_id = max([int(flavor.id) for flavor in flavors])
for server in servers:
max_id += 1
server.flavor["id"] = max_id
api.server_list(IsA(http.HttpRequest)).AndReturn(servers)
api.flavor_list(IsA(http.HttpRequest)).AndReturn(flavors)
for server in servers:
api.flavor_get(IsA(http.HttpRequest), server.flavor["id"]). \
AndRaise(self.exceptions.nova)
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:nova:instances:index'))
instances = res.context['instances_table'].data
self.assertTemplateUsed(res, 'nova/instances/index.html')
self.assertMessageCount(res, error=len(servers))
self.assertItemsEqual(instances, self.servers.list())
@test.create_stubs({api: ('server_list',
'flavor_list',
'server_delete',
'volume_list',)})
'server_delete',)})
def test_terminate_instance(self):
server = self.servers.first()
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
api.flavor_list(IgnoreArg()).AndReturn(self.flavors.list())
api.server_delete(IsA(http.HttpRequest), server.id)
@ -56,12 +129,10 @@ class InstanceViewTests(test.TestCase):
@test.create_stubs({api: ('server_list',
'flavor_list',
'server_delete',
'volume_list',)})
'server_delete',)})
def test_terminate_instance_exception(self):
server = self.servers.first()
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
api.flavor_list(IgnoreArg()).AndReturn(self.flavors.list())
api.server_delete(IsA(http.HttpRequest), server.id) \
@ -76,13 +147,11 @@ class InstanceViewTests(test.TestCase):
@test.create_stubs({api: ('server_pause',
'server_list',
'volume_list',
'flavor_list',)})
def test_pause_instance(self):
server = self.servers.first()
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
api.server_pause(IsA(http.HttpRequest), server.id)
@ -95,13 +164,11 @@ class InstanceViewTests(test.TestCase):
@test.create_stubs({api: ('server_pause',
'server_list',
'volume_list',
'flavor_list',)})
def test_pause_instance_exception(self):
server = self.servers.first()
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
api.server_pause(IsA(http.HttpRequest), server.id) \
.AndRaise(self.exceptions.nova)
@ -113,8 +180,7 @@ class InstanceViewTests(test.TestCase):
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api: ('volume_list',
'server_unpause',
@test.create_stubs({api: ('server_unpause',
'server_list',
'flavor_list',)})
def test_unpause_instance(self):
@ -122,7 +188,6 @@ class InstanceViewTests(test.TestCase):
server.status = "PAUSED"
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
api.server_unpause(IsA(http.HttpRequest), server.id)
@ -133,8 +198,7 @@ class InstanceViewTests(test.TestCase):
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api: ('volume_list',
'server_unpause',
@test.create_stubs({api: ('server_unpause',
'server_list',
'flavor_list',)})
def test_unpause_instance_exception(self):
@ -142,7 +206,6 @@ class InstanceViewTests(test.TestCase):
server.status = "PAUSED"
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
api.server_unpause(IsA(http.HttpRequest), server.id) \
.AndRaise(self.exceptions.nova)
@ -154,15 +217,13 @@ class InstanceViewTests(test.TestCase):
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api: ('volume_list',
'server_reboot',
@test.create_stubs({api: ('server_reboot',
'server_list',
'flavor_list',)})
def test_reboot_instance(self):
server = self.servers.first()
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
api.server_reboot(IsA(http.HttpRequest), server.id)
@ -173,15 +234,13 @@ class InstanceViewTests(test.TestCase):
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api: ('volume_list',
'server_reboot',
@test.create_stubs({api: ('server_reboot',
'server_list',
'flavor_list',)})
def test_reboot_instance_exception(self):
server = self.servers.first()
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
api.server_reboot(IsA(http.HttpRequest), server.id) \
.AndRaise(self.exceptions.nova)
@ -193,15 +252,13 @@ class InstanceViewTests(test.TestCase):
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api: ('volume_list',
'server_suspend',
@test.create_stubs({api: ('server_suspend',
'server_list',
'flavor_list',)})
def test_suspend_instance(self):
server = self.servers.first()
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
api.server_suspend(IsA(http.HttpRequest), unicode(server.id))
@ -212,15 +269,13 @@ class InstanceViewTests(test.TestCase):
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api: ('volume_list',
'server_suspend',
@test.create_stubs({api: ('server_suspend',
'server_list',
'flavor_list',)})
def test_suspend_instance_exception(self):
server = self.servers.first()
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
api.server_suspend(IsA(http.HttpRequest),
unicode(server.id)).AndRaise(self.exceptions.nova)
@ -232,8 +287,7 @@ class InstanceViewTests(test.TestCase):
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api: ('volume_list',
'server_resume',
@test.create_stubs({api: ('server_resume',
'server_list',
'flavor_list',)})
def test_resume_instance(self):
@ -241,7 +295,6 @@ class InstanceViewTests(test.TestCase):
server.status = "SUSPENDED"
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
api.server_resume(IsA(http.HttpRequest), unicode(server.id))
@ -252,8 +305,7 @@ class InstanceViewTests(test.TestCase):
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api: ('volume_list',
'server_resume',
@test.create_stubs({api: ('server_resume',
'server_list',
'flavor_list',)})
def test_resume_instance_exception(self):
@ -261,7 +313,6 @@ class InstanceViewTests(test.TestCase):
server.status = "SUSPENDED"
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
api.server_resume(IsA(http.HttpRequest),
unicode(server.id)).AndRaise(self.exceptions.nova)
@ -291,7 +342,7 @@ class InstanceViewTests(test.TestCase):
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:instances:detail',
url = reverse('horizon:nova:instances:detail',
args=[server.id])
res = self.client.get(url)
@ -315,7 +366,7 @@ class InstanceViewTests(test.TestCase):
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:instances:detail',
url = reverse('horizon:nova:instances:detail',
args=[server.id])
res = self.client.get(url)
@ -342,7 +393,7 @@ class InstanceViewTests(test.TestCase):
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:instances:detail',
url = reverse('horizon:nova:instances:detail',
args=[server.id])
tg = InstanceDetailTabs(self.request, instance=server)
qs = "?%s=%s" % (tg.param_name, tg.get_tab("overview").get_id())
@ -368,7 +419,7 @@ class InstanceViewTests(test.TestCase):
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:instances:console',
url = reverse('horizon:nova:instances:console',
args=[server.id])
tg = InstanceDetailTabs(self.request, instance=server)
qs = "?%s=%s" % (tg.param_name, tg.get_tab("log").get_id())
@ -388,7 +439,7 @@ class InstanceViewTests(test.TestCase):
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:instances:console',
url = reverse('horizon:nova:instances:console',
args=[server.id])
tg = InstanceDetailTabs(self.request, instance=server)
qs = "?%s=%s" % (tg.param_name, tg.get_tab("log").get_id())
@ -409,7 +460,7 @@ class InstanceViewTests(test.TestCase):
api.server_vnc_console(IgnoreArg(), server.id).AndReturn(console_mock)
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:instances:vnc',
url = reverse('horizon:nova:instances:vnc',
args=[server.id])
res = self.client.get(url)
redirect = CONSOLE_OUTPUT + '&title=%s(1)' % server.name
@ -424,7 +475,7 @@ class InstanceViewTests(test.TestCase):
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:instances:vnc',
url = reverse('horizon:nova:instances:vnc',
args=[server.id])
res = self.client.get(url)
@ -437,8 +488,7 @@ class InstanceViewTests(test.TestCase):
'volume_snapshot_list',
'server_list',
'flavor_list',
'server_delete',
'volume_list',)})
'server_delete',)})
def test_create_instance_snapshot(self):
server = self.servers.first()
snapshot_server = deepcopy(server)
@ -455,7 +505,6 @@ class InstanceViewTests(test.TestCase):
api.image_list_detailed(IsA(http.HttpRequest),
marker=None).AndReturn([[], False])
api.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([])
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_list(IsA(http.HttpRequest)).AndReturn([snapshot_server])
api.flavor_list(IgnoreArg()).AndReturn(self.flavors.list())
@ -483,12 +532,12 @@ class InstanceViewTests(test.TestCase):
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:instances:update',
url = reverse('horizon:nova:instances:update',
args=[server.id])
res = self.client.get(url)
self.assertTemplateUsed(res,
'nova/instances_and_volumes/instances/update.html')
'nova/instances/update.html')
@test.create_stubs({api: ('server_get',)})
def test_instance_update_get_server_get_exception(self):
@ -499,7 +548,7 @@ class InstanceViewTests(test.TestCase):
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:instances:update',
url = reverse('horizon:nova:instances:update',
args=[server.id])
res = self.client.get(url)
@ -518,7 +567,7 @@ class InstanceViewTests(test.TestCase):
'instance': server.id,
'name': server.name,
'tenant_id': self.tenant.id}
url = reverse('horizon:nova:instances_and_volumes:instances:update',
url = reverse('horizon:nova:instances:update',
args=[server.id])
res = self.client.post(url, formData)
@ -538,7 +587,7 @@ class InstanceViewTests(test.TestCase):
'instance': server.id,
'name': server.name,
'tenant_id': self.tenant.id}
url = reverse('horizon:nova:instances_and_volumes:instances:update',
url = reverse('horizon:nova:instances:update',
args=[server.id])
res = self.client.post(url, formData)
@ -578,14 +627,14 @@ class InstanceViewTests(test.TestCase):
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:instances:launch')
url = reverse('horizon:nova:instances:launch')
params = urlencode({"source_type": "image_id",
"source_id": image.id})
res = self.client.get("%s?%s" % (url, params))
workflow = res.context['workflow']
self.assertTemplateUsed(res,
'nova/instances_and_volumes/instances/launch.html')
'nova/instances/launch.html')
self.assertEqual(res.context['workflow'].name, LaunchInstance.name)
step = workflow.get_step("setinstancedetailsaction")
self.assertEqual(step.action.initial['image_id'], image.id)
@ -655,12 +704,12 @@ class InstanceViewTests(test.TestCase):
'volume_id': volume_choice,
'device_name': device_name,
'count': 1}
url = reverse('horizon:nova:instances_and_volumes:instances:launch')
url = reverse('horizon:nova:instances:launch')
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res,
reverse('horizon:nova:instances_and_volumes:index'))
reverse('horizon:nova:instances:index'))
@test.create_stubs({api.glance: ('image_list_detailed',),
api.nova: ('tenant_quota_usages',
@ -693,11 +742,11 @@ class InstanceViewTests(test.TestCase):
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:instances:launch')
url = reverse('horizon:nova:instances:launch')
res = self.client.get(url)
self.assertTemplateUsed(res,
'nova/instances_and_volumes/instances/launch.html')
'nova/instances/launch.html')
@test.create_stubs({api.glance: ('image_list_detailed',),
api.nova: ('flavor_list',
@ -751,7 +800,7 @@ class InstanceViewTests(test.TestCase):
'groups': sec_group.name,
'volume_type': '',
'count': 1}
url = reverse('horizon:nova:instances_and_volumes:instances:launch')
url = reverse('horizon:nova:instances:launch')
res = self.client.post(url, form_data)
self.assertRedirectsNoFollow(res, INDEX_URL)
@ -810,7 +859,7 @@ class InstanceViewTests(test.TestCase):
'volume_id': volume_choice,
'device_name': device_name,
'count': 0}
url = reverse('horizon:nova:instances_and_volumes:instances:launch')
url = reverse('horizon:nova:instances:launch')
res = self.client.post(url, form_data)
self.assertContains(res, "greater than or equal to 1")

View File

@ -20,16 +20,16 @@
from django.conf.urls.defaults import patterns, url
from .views import UpdateView, DetailView, LaunchInstanceView
from .views import IndexView, UpdateView, DetailView, LaunchInstanceView
INSTANCES = r'^(?P<instance_id>[^/]+)/%s$'
urlpatterns = patterns(
'horizon.dashboards.nova.instances_and_volumes.instances.views',
urlpatterns = patterns('horizon.dashboards.nova.instances.views',
url(r'^$', IndexView.as_view(), name='index'),
url(r'^launch$', LaunchInstanceView.as_view(), name='launch'),
url(INSTANCES % 'detail', DetailView.as_view(), name='detail'),
url(r'^(?P<instance_id>[^/]+)/$', DetailView.as_view(), name='detail'),
url(INSTANCES % 'update', UpdateView.as_view(), name='update'),
url(INSTANCES % 'console', 'console', name='console'),
url(INSTANCES % 'vnc', 'vnc', name='vnc')

View File

@ -26,24 +26,66 @@ import logging
from django import http
from django import shortcuts
from django.core.urlresolvers import reverse
from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext_lazy as _
from horizon import api
from horizon import exceptions
from horizon import forms
from horizon import tabs
from horizon import tables
from horizon import workflows
from .forms import UpdateInstance
from .tabs import InstanceDetailTabs
from .tables import InstancesTable
from .workflows import LaunchInstance
LOG = logging.getLogger(__name__)
class IndexView(tables.DataTableView):
table_class = InstancesTable
template_name = 'nova/instances/index.html'
def get_data(self):
# Gather our instances
try:
instances = api.server_list(self.request)
except:
instances = []
exceptions.handle(self.request,
_('Unable to retrieve instances.'))
# Gather our flavors and correlate our instances to them
if instances:
try:
flavors = api.flavor_list(self.request)
except:
flavors = []
exceptions.handle(self.request, ignore=True)
full_flavors = SortedDict([(str(flavor.id), flavor)
for flavor in flavors])
# Loop through instances to get flavor info.
for instance in instances:
try:
flavor_id = instance.flavor["id"]
if flavor_id in full_flavors:
instance.full_flavor = full_flavors[flavor_id]
else:
# If the flavor_id is not in full_flavors list,
# get it via nova api.
instance.full_flavor = api.flavor_get(self.request,
flavor_id)
except:
msg = _('Unable to retrieve instance size information.')
exceptions.handle(self.request, msg)
return instances
class LaunchInstanceView(workflows.WorkflowView):
workflow_class = LaunchInstance
template_name = "nova/instances_and_volumes/instances/launch.html"
template_name = "nova/instances/launch.html"
def get_initial(self):
initial = super(LaunchInstanceView, self).get_initial()
@ -75,14 +117,14 @@ def vnc(request, instance_id):
return shortcuts.redirect(console.url +
("&title=%s(%s)" % (instance.name, instance_id)))
except:
redirect = reverse("horizon:nova:instances_and_volumes:index")
redirect = reverse("horizon:nova:instances:index")
msg = _('Unable to get VNC console for instance "%s".') % instance_id
exceptions.handle(request, msg, redirect=redirect)
class UpdateView(forms.ModalFormView):
form_class = UpdateInstance
template_name = 'nova/instances_and_volumes/instances/update.html'
template_name = 'nova/instances/update.html'
context_object_name = 'instance'
def get_object(self, *args, **kwargs):
@ -91,7 +133,7 @@ class UpdateView(forms.ModalFormView):
try:
self.object = api.server_get(self.request, instance_id)
except:
redirect = reverse("horizon:nova:instances_and_volumes:index")
redirect = reverse("horizon:nova:instances:index")
msg = _('Unable to retrieve instance details.')
exceptions.handle(self.request, msg, redirect=redirect)
return self.object
@ -104,7 +146,7 @@ class UpdateView(forms.ModalFormView):
class DetailView(tabs.TabView):
tab_group_class = InstanceDetailTabs
template_name = 'nova/instances_and_volumes/instances/detail.html'
template_name = 'nova/instances/detail.html'
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
@ -125,7 +167,7 @@ class DetailView(tabs.TabView):
instance.security_groups = api.server_security_groups(
self.request, instance_id)
except:
redirect = reverse('horizon:nova:instances_and_volumes:index')
redirect = reverse('horizon:nova:instances:index')
exceptions.handle(self.request,
_('Unable to retrieve details for '
'instance "%s".') % instance_id,

View File

@ -82,7 +82,8 @@ class VolumeOptionsAction(workflows.Action):
class Meta:
name = _("Volume Options")
help_text_template = ("nova/instances_and_volumes/instances/"
services = ('volume',)
help_text_template = ("nova/instances/"
"_launch_volumes_help.html")
def clean(self):
@ -176,7 +177,7 @@ class SetInstanceDetailsAction(workflows.Action):
class Meta:
name = _("Details")
help_text_template = ("nova/instances_and_volumes/instances/"
help_text_template = ("nova/instances/"
"_launch_details_help.html")
def clean(self):
@ -378,7 +379,7 @@ class CustomizeAction(workflows.Action):
class Meta:
name = _("Post-Creation")
help_text_template = ("nova/instances_and_volumes/instances/"
help_text_template = ("nova/instances/"
"_launch_customize_help.html")
@ -393,7 +394,7 @@ class LaunchInstance(workflows.Workflow):
finalize_button_name = _("Launch")
success_message = _('Launched %s named "%s".')
failure_message = _('Unable to launch %s named "%s".')
success_url = "horizon:nova:instances_and_volumes:index"
success_url = "horizon:nova:instances:index"
default_steps = (SelectProjectUser,
SetInstanceDetails,
SetAccessControls,

View File

@ -1,17 +0,0 @@
{% extends 'nova/base.html' %}
{% load i18n %}
{% block title %}Instances &amp; Volumes{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Instances &amp; Volumes") %}
{% endblock page_header %}
{% block dash_main %}
<div id="instances">
{{ instances_table.render }}
</div>
<div id="volumes">
{{ volumes_table.render }}
</div>
{% endblock %}

View File

@ -1,153 +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.
#
# 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.core.urlresolvers import reverse
from django.utils.datastructures import SortedDict
from mox import IsA
from horizon import api
from horizon import test
class InstancesAndVolumesViewTest(test.TestCase):
@test.create_stubs({api: ('flavor_list', 'server_list', 'volume_list',)})
def test_index(self):
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
self.mox.ReplayAll()
res = self.client.get(
reverse('horizon:nova:instances_and_volumes:index'))
self.assertTemplateUsed(res,
'nova/instances_and_volumes/index.html')
instances = res.context['instances_table'].data
volumes = res.context['volumes_table'].data
self.assertItemsEqual(instances, self.servers.list())
self.assertItemsEqual(volumes, self.volumes.list())
@test.create_stubs({api: ('flavor_list', 'server_list', 'volume_list',)})
def test_attached_volume(self):
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
api.volume_list(IsA(http.HttpRequest)) \
.AndReturn(self.volumes.list()[1:3])
self.mox.ReplayAll()
res = self.client.get(
reverse('horizon:nova:instances_and_volumes:index'))
self.assertTemplateUsed(res,
'nova/instances_and_volumes/index.html')
instances = res.context['instances_table'].data
resp_volumes = res.context['volumes_table'].data
self.assertItemsEqual(instances, self.servers.list())
self.assertItemsEqual(resp_volumes, self.volumes.list()[1:3])
self.assertContains(res, ">My Volume<", 1, 200)
self.assertContains(res, ">30GB<", 1, 200)
self.assertContains(res, ">3b189ac8-9166-ac7f-90c9-16c8bf9e01ac<",
1,
200)
self.assertContains(res, ">10GB<", 1, 200)
self.assertContains(res, ">In-Use<", 2, 200)
self.assertContains(res, "on /dev/hda", 1, 200)
self.assertContains(res, "on /dev/hdk", 1, 200)
@test.create_stubs({api: ('server_list', 'volume_list',)})
def test_index_server_list_exception(self):
api.server_list(IsA(http.HttpRequest)).AndRaise(self.exceptions.nova)
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
self.mox.ReplayAll()
res = self.client.get(
reverse('horizon:nova:instances_and_volumes:index'))
self.assertTemplateUsed(res,
'nova/instances_and_volumes/index.html')
self.assertEqual(len(res.context['instances_table'].data), 0)
@test.create_stubs({api: ('flavor_list', 'server_list',
'flavor_get', 'volume_list',)})
def test_index_flavor_list_exception(self):
servers = self.servers.list()
flavors = self.flavors.list()
volumes = self.volumes.list()
full_flavors = SortedDict([(f.id, f) for f in flavors])
api.server_list(IsA(http.HttpRequest)).AndReturn(servers)
api.flavor_list(IsA(http.HttpRequest)).AndRaise(self.exceptions.nova)
for server in servers:
api.flavor_get(IsA(http.HttpRequest), server.flavor["id"]). \
AndReturn(full_flavors[server.flavor["id"]])
api.volume_list(IsA(http.HttpRequest)).AndReturn(volumes)
self.mox.ReplayAll()
res = self.client.get(
reverse('horizon:nova:instances_and_volumes:index'))
self.assertTemplateUsed(res,
'nova/instances_and_volumes/index.html')
instances = res.context['instances_table'].data
volumes = res.context['volumes_table'].data
self.assertItemsEqual(instances, self.servers.list())
self.assertItemsEqual(volumes, self.volumes.list())
@test.create_stubs({api: ('flavor_list', 'server_list',
'flavor_get', 'volume_list',)})
def test_index_flavor_get_exception(self):
servers = self.servers.list()
flavors = self.flavors.list()
volumes = self.volumes.list()
max_id = max([int(flavor.id) for flavor in flavors])
for server in servers:
max_id += 1
server.flavor["id"] = max_id
api.server_list(IsA(http.HttpRequest)).AndReturn(servers)
api.flavor_list(IsA(http.HttpRequest)).AndReturn(flavors)
for server in servers:
api.flavor_get(IsA(http.HttpRequest), server.flavor["id"]). \
AndRaise(self.exceptions.nova)
api.volume_list(IsA(http.HttpRequest)).AndReturn(volumes)
self.mox.ReplayAll()
res = self.client.get(
reverse('horizon:nova:instances_and_volumes:index'))
instances = res.context['instances_table'].data
volumes = res.context['volumes_table'].data
self.assertTemplateUsed(res,
'nova/instances_and_volumes/index.html')
self.assertMessageCount(res, error=len(servers))
self.assertItemsEqual(instances, self.servers.list())
self.assertItemsEqual(volumes, self.volumes.list())

View File

@ -1,107 +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 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 Instances and Volumes.
"""
import logging
from django.utils.translation import ugettext_lazy as _
from django.utils.datastructures import SortedDict
from horizon import api
from horizon import exceptions
from horizon import tables
from .instances.tables import InstancesTable
from .volumes.tables import VolumesTable
LOG = logging.getLogger(__name__)
class IndexView(tables.MultiTableView):
table_classes = (InstancesTable, VolumesTable)
template_name = 'nova/instances_and_volumes/index.html'
def get_instances_data(self):
if not hasattr(self, "_instances"):
# Gather our instances
try:
instances = self._get_instances()
except:
instances = []
exceptions.handle(self.request,
_('Unable to retrieve instances.'))
# Gather our flavors and correlate our instances to them
if instances:
try:
flavors = api.flavor_list(self.request)
except:
# If fails to retrieve flavor list, creates an empty list.
flavors = []
full_flavors = SortedDict([(str(flavor.id), flavor)
for flavor in flavors])
# Loop through instances to get flavor info.
for instance in instances:
try:
flavor_id = instance.flavor["id"]
if flavor_id in full_flavors:
instance.full_flavor = full_flavors[flavor_id]
else:
# If the flavor_id is not in full_flavors list,
# gets it via nova api.
instance.full_flavor = api.flavor_get(
self.request, flavor_id)
except:
msg = _('Unable to retrieve instance \
size information.')
exceptions.handle(self.request, msg)
self._instances = instances
return self._instances
def get_volumes_data(self):
# Gather our volumes
try:
volumes = api.volume_list(self.request)
instances = SortedDict([(inst.id, inst) for inst in
self._get_instances()])
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
description = getattr(volume, 'display_description', '')
for att in volume.attachments:
server_id = att.get('server_id', None)
att['instance'] = instances.get(server_id, None)
except:
volumes = []
exceptions.handle(self.request,
_('Unable to retrieve volume list.'))
return volumes
def _get_instances(self):
if not hasattr(self, "_instances_list"):
self._instances_list = api.server_list(self.request)
return self._instances_list

View File

@ -56,10 +56,9 @@ class CreateForm(forms.SelfHandlingForm):
return self.api_error(e.messages[0])
except:
exceptions.handle(request, ignore=True)
return self.api_error(_("Unable to create volume."))
return shortcuts.redirect("horizon:nova:instances_and_volumes:index")
return shortcuts.redirect("horizon:nova:volumes:index")
class AttachForm(forms.SelfHandlingForm):
@ -116,8 +115,7 @@ class AttachForm(forms.SelfHandlingForm):
except:
exceptions.handle(request,
_('Unable to attach volume.'))
return shortcuts.redirect(
"horizon:nova:instances_and_volumes:index")
return shortcuts.redirect("horizon:nova:volumes:index")
class CreateSnapshotForm(forms.SelfHandlingForm):

View File

@ -1,9 +1,5 @@
# 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.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -18,15 +14,16 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls.defaults import *
from django.utils.translation import ugettext_lazy as _
from .instances import urls as instance_urls
from .views import IndexView
from .volumes import urls as volume_urls
import horizon
from horizon.dashboards.nova import dashboard
urlpatterns = patterns('horizon.dashboards.nova.instances_and_volumes',
url(r'^$', IndexView.as_view(), name='index'),
url(r'^instances/', include(instance_urls, namespace='instances')),
url(r'^volumes/', include(volume_urls, namespace='volumes')),
)
class Volumes(horizon.Panel):
name = _("Volumes")
slug = 'volumes'
services = ('volume',)
dashboard.Nova.register(Volumes)

View File

@ -29,7 +29,6 @@ from horizon import tables
LOG = logging.getLogger(__name__)
URL_PREFIX = "horizon:nova:instances_and_volumes"
DELETABLE_STATES = ("available", "error")
@ -49,14 +48,14 @@ class DeleteVolume(tables.DeleteAction):
class CreateVolume(tables.LinkAction):
name = "create"
verbose_name = _("Create Volume")
url = "%s:volumes:create" % URL_PREFIX
url = "horizon:nova:volumes:create"
classes = ("ajax-modal", "btn-create")
class EditAttachments(tables.LinkAction):
name = "attachments"
verbose_name = _("Edit Attachments")
url = "%s:volumes:attach" % URL_PREFIX
url = "horizon:nova:volumes:attach"
classes = ("ajax-modal", "btn-edit")
def allowed(self, request, volume=None):
@ -66,7 +65,7 @@ class EditAttachments(tables.LinkAction):
class CreateSnapshot(tables.LinkAction):
name = "snapshots"
verbose_name = _("Create Snapshot")
url = "%s:volumes:create_snapshot" % URL_PREFIX
url = "horizon:nova:volumes:create_snapshot"
classes = ("ajax-modal", "btn-camera")
def allowed(self, request, volume=None):
@ -87,7 +86,7 @@ def get_size(volume):
def get_attachment_name(request, attachment):
server_id = attachment.get("server_id", None)
if "instance" in attachment:
if "instance" in attachment and attachment['instance']:
name = attachment["instance"].name
else:
try:
@ -98,7 +97,7 @@ def get_attachment_name(request, attachment):
exceptions.handle(request, _("Unable to retrieve "
"attachment information."))
try:
url = reverse("%s:instances:detail" % URL_PREFIX, args=(server_id,))
url = reverse("horizon:nova:instances:detail", args=(server_id,))
instance = '<a href="%s">%s</a>' % (url, name)
except NoReverseMatch:
instance = name
@ -116,7 +115,7 @@ class AttachmentColumn(tables.Column):
attachments = []
# Filter out "empty" attachments which the client returns...
for attachment in [att for att in volume.attachments if att]:
# When a volume is first attached it may return the server_id
# When a volume is attached it may return the server_id
# without the server name...
instance = get_attachment_name(request, attachment)
vals = {"instance": instance,
@ -132,8 +131,9 @@ class VolumesTableBase(tables.DataTable):
("creating", None),
("error", False),
)
name = tables.Column("display_name", verbose_name=_("Name"),
link="%s:volumes:detail" % URL_PREFIX)
name = tables.Column("display_name",
verbose_name=_("Name"),
link="horizon:nova:volumes:detail")
description = tables.Column("display_description",
verbose_name=_("Description"),
truncate=40)
@ -151,7 +151,7 @@ class VolumesTableBase(tables.DataTable):
class VolumesTable(VolumesTableBase):
name = tables.Column("display_name",
verbose_name=_("Name"),
link="%s:volumes:detail" % URL_PREFIX)
link="horizon:nova:volumes:detail")
attachments = AttachmentColumn("attachments",
verbose_name=_("Attached To"))
@ -177,7 +177,7 @@ class DetachVolume(tables.BatchAction):
api.volume_detach(request, attachment.get('server_id', None), obj_id)
def get_success_url(self, request):
return reverse('%s:index' % URL_PREFIX)
return reverse('horizon:nova:volumes:index')
class AttachedInstanceColumn(tables.Column):

View File

@ -25,7 +25,7 @@ from horizon import tabs
class OverviewTab(tabs.Tab):
name = _("Overview")
slug = "overview"
template_name = ("nova/instances_and_volumes/volumes/"
template_name = ("nova/volumes/"
"_detail_overview.html")
def get_context_data(self, request):
@ -36,7 +36,7 @@ class OverviewTab(tabs.Tab):
att['instance'] = api.nova.server_get(request,
att['server_id'])
except:
redirect = reverse('horizon:nova:instances_and_volumes:index')
redirect = reverse('horizon:nova:volumes:index')
exceptions.handle(self.request,
_('Unable to retrieve volume details.'),
redirect=redirect)

View File

@ -2,7 +2,7 @@
{% load i18n %}
{% block form_id %}attach_volume_form{% endblock %}
{% block form_action %}{% url horizon:nova:instances_and_volumes:volumes:attach volume.id %}{% endblock %}
{% block form_action %}{% url horizon:nova:volumes:attach volume.id %}{% endblock %}
{% block form_class %}{{ block.super }} horizontal split_half{% endblock %}
{% block modal_id %}attach_volume_modal{% endblock %}
@ -17,5 +17,5 @@
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Attach Volume" %}" />
<a href="{% url horizon:nova:instances_and_volumes:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url horizon:nova:volumes:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -2,7 +2,7 @@
{% load i18n horizon humanize %}
{% block form_id %}{% endblock %}
{% block form_action %}{% url horizon:nova:instances_and_volumes:volumes:create %}{% endblock %}
{% block form_action %}{% url horizon:nova:volumes:create %}{% endblock %}
{% block modal_id %}create_volume_modal{% endblock %}
{% block modal-header %}{% trans "Create Volume" %}{% endblock %}
@ -53,5 +53,5 @@
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Volume" %}" />
<a href="{% url horizon:nova:instances_and_volumes:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url horizon:nova:volumes:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -2,7 +2,7 @@
{% load i18n %}
{% block form_id %}{% endblock %}
{% block form_action %}{% url horizon:nova:instances_and_volumes:volumes:create_snapshot volume_id %}{% endblock %}
{% block form_action %}{% url horizon:nova:volumes:create_snapshot volume_id %}{% endblock %}
{% block modal_id %}create_volume_snapshot_modal{% endblock %}
{% block modal-header %}{% trans "Create Volume Snapshot" %}{% endblock %}
@ -21,5 +21,5 @@
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Volume Snapshot" %}" />
<a href="{% url horizon:nova:instances_and_volumes:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url horizon:nova:volumes:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -37,7 +37,7 @@
{% for attachment in volume.attachments %}
<dt>{% trans "Attached To" %}</dt>
<dd>
{% url horizon:nova:instances_and_volumes:instances:detail attachment.server_id as instance_url%}
{% url horizon:nova:instances:detail attachment.server_id as instance_url%}
<a href="{{ instance_url }}">{{ attachment.instance.name }}</a>
<span> {% trans "on" %} {{ attachment.device }}</span>
</dd>

View File

@ -7,5 +7,5 @@
{% endblock page_header %}
{% block dash_main %}
{% include 'nova/instances_and_volumes/volumes/_attach.html' %}
{% include 'nova/volumes/_attach.html' %}
{% endblock %}

View File

@ -7,5 +7,5 @@
{% endblock page_header %}
{% block dash_main %}
{% include 'nova/instances_and_volumes/volumes/_create.html' %}
{% include 'nova/volumes/_create.html' %}
{% endblock %}

View File

@ -7,5 +7,5 @@
{% endblock page_header %}
{% block dash_main %}
{% include 'nova/instances_and_volumes/volumes/_create_snapshot.html' %}
{% include 'nova/volumes/_create_snapshot.html' %}
{% endblock %}

View File

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

View File

@ -43,10 +43,10 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:volumes:create')
url = reverse('horizon:nova:volumes:create')
res = self.client.post(url, formData)
redirect_url = reverse('horizon:nova:instances_and_volumes:index')
redirect_url = reverse('horizon:nova:volumes:index')
self.assertRedirectsNoFollow(res, redirect_url)
@test.create_stubs({api: ('tenant_quota_usages',)})
@ -62,7 +62,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:volumes:create')
url = reverse('horizon:nova:volumes:create')
res = self.client.post(url, formData)
expected_error = [u'A volume of 5000GB cannot be created as you only'
@ -83,7 +83,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:volumes:create')
url = reverse('horizon:nova:volumes:create')
res = self.client.post(url, formData)
expected_error = [u'You are already using all of your available'
@ -101,7 +101,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:volumes:attach',
url = reverse('horizon:nova:volumes:attach',
args=[volume.id])
res = self.client.get(url)
# Asserting length of 2 accounts for the one instance option,
@ -123,7 +123,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:volumes:attach',
url = reverse('horizon:nova:volumes:attach',
args=[volume.id])
res = self.client.get(url)
@ -147,7 +147,7 @@ class VolumeViewTests(test.TestCase):
self.mox.ReplayAll()
url = reverse('horizon:nova:instances_and_volumes:volumes:detail',
url = reverse('horizon:nova:volumes:detail',
args=[volume.id])
res = self.client.get(url)
@ -158,7 +158,10 @@ class VolumeViewTests(test.TestCase):
200)
self.assertContains(res, "<dd>Available</dd>", 1, 200)
self.assertContains(res, "<dd>40 GB</dd>", 1, 200)
self.assertContains(res, "<a href=\"/nova/instances_and_volumes/"
"instances/1/detail\">server_1</a>", 1, 200)
self.assertContains(res,
("<a href=\"/nova/instances/1/\">%s</a>"
% server.name),
1,
200)
self.assertNoMessages()

View File

@ -16,12 +16,12 @@
from django.conf.urls.defaults import patterns, url
from .views import (CreateView, EditAttachmentsView, DetailView,
from .views import (IndexView, CreateView, EditAttachmentsView, DetailView,
CreateSnapshotView)
urlpatterns = patterns(
'horizon.dashboards.nova.instances_and_volumes.volumes.views',
urlpatterns = patterns('horizon.dashboards.nova.volumes.views',
url(r'^$', IndexView.as_view(), name='index'),
url(r'^create/$', CreateView.as_view(), name='create'),
url(r'^(?P<volume_id>[^/]+)/attach/$',
EditAttachmentsView.as_view(),
@ -29,7 +29,7 @@ urlpatterns = patterns(
url(r'^(?P<volume_id>[^/]+)/create_snapshot/$',
CreateSnapshotView.as_view(),
name='create_snapshot'),
url(r'^(?P<volume_id>[^/]+)/detail/$',
url(r'^(?P<volume_id>[^/]+)/$',
DetailView.as_view(),
name='detail'),
)

View File

@ -21,6 +21,7 @@ Views for managing Nova volumes.
import logging
from django.utils.translation import ugettext_lazy as _
from django.utils.datastructures import SortedDict
from horizon import api
from horizon import exceptions
@ -28,21 +29,54 @@ from horizon import forms
from horizon import tables
from horizon import tabs
from .forms import CreateForm, AttachForm, CreateSnapshotForm
from .tables import AttachmentsTable
from .tables import AttachmentsTable, VolumesTable
from .tabs import VolumeDetailTabs
LOG = logging.getLogger(__name__)
class IndexView(tables.DataTableView):
table_class = VolumesTable
template_name = 'nova/volumes/index.html'
def get_data(self):
# Gather our volumes
try:
volumes = api.volume_list(self.request)
except:
volumes = []
exceptions.handle(self.request,
_('Unable to retrieve volume list.'))
try:
instance_list = api.server_list(self.request)
except:
instance_list = []
exceptions.handle(self.request,
_("Unable to retrieve volume/instance "
"attachment information"))
instances = SortedDict([(inst.id, inst) for inst in instance_list])
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
for att in volume.attachments:
server_id = att.get('server_id', None)
att['instance'] = instances.get(server_id, None)
return volumes
class DetailView(tabs.TabView):
tab_group_class = VolumeDetailTabs
template_name = 'nova/instances_and_volumes/volumes/detail.html'
template_name = 'nova/volumes/detail.html'
class CreateView(forms.ModalFormView):
form_class = CreateForm
template_name = 'nova/instances_and_volumes/volumes/create.html'
template_name = 'nova/volumes/create.html'
def get_context_data(self, **kwargs):
context = super(CreateView, self).get_context_data(**kwargs)
@ -56,7 +90,7 @@ class CreateView(forms.ModalFormView):
class CreateSnapshotView(forms.ModalFormView):
form_class = CreateSnapshotForm
template_name = 'nova/instances_and_volumes/volumes/create_snapshot.html'
template_name = 'nova/volumes/create_snapshot.html'
def get_context_data(self, **kwargs):
return {'volume_id': kwargs['volume_id']}
@ -67,7 +101,7 @@ class CreateSnapshotView(forms.ModalFormView):
class EditAttachmentsView(tables.DataTableView):
table_class = AttachmentsTable
template_name = 'nova/instances_and_volumes/volumes/attach.html'
template_name = 'nova/volumes/attach.html'
def get_object(self):
if not hasattr(self, "_object"):
@ -113,7 +147,7 @@ class EditAttachmentsView(tables.DataTableView):
context['form'] = self.form
if request.is_ajax():
context['hide'] = True
self.template_name = ('nova/instances_and_volumes/volumes'
self.template_name = ('nova/volumes'
'/_attach.html')
return self.render_to_response(context)

View File

@ -22,8 +22,8 @@ import horizon
class SystemPanels(horizon.PanelGroup):
slug = "syspanel"
name = _("System Panel")
panels = ('overview', 'instances', 'services', 'flavors', 'images',
'projects', 'users', 'quotas',)
panels = ('overview', 'instances', 'volumes', 'services', 'flavors',
'images', 'projects', 'users', 'quotas',)
class Syspanel(horizon.Dashboard):

View File

@ -22,8 +22,8 @@ from django.utils.translation import ugettext_lazy as _
from horizon import api
from horizon import tables
from horizon.dashboards.nova.instances_and_volumes.instances.tables import (
TerminateInstance, EditInstance, ConsoleLink, LogLink, SnapshotLink,
from horizon.dashboards.nova.instances.tables import (TerminateInstance,
EditInstance, ConsoleLink, LogLink, SnapshotLink,
TogglePause, ToggleSuspend, RebootInstance, get_size, UpdateRow,
LaunchLink, get_ips, get_power_state)
from horizon.utils.filters import replace_underscores
@ -69,8 +69,7 @@ class SyspanelInstancesTable(tables.DataTable):
verbose_name=_("Host"),
classes=('nowrap-col',))
name = tables.Column("name",
link=("horizon:nova:instances_and_volumes:"
"instances:detail"),
link=("horizon:nova:instances:detail"),
verbose_name=_("Instance Name"))
ip = tables.Column(get_ips, verbose_name=_("IP Address"))
size = tables.Column(get_size,

View File

@ -28,8 +28,8 @@ from horizon import api
from horizon import exceptions
from horizon import tables
from horizon.dashboards.syspanel.instances.tables import SyspanelInstancesTable
from horizon.dashboards.nova.instances_and_volumes.instances.views import (
console, DetailView, vnc, LaunchInstanceView)
from horizon.dashboards.nova.instances.views import (console, DetailView,
vnc, LaunchInstanceView)
from .workflows import AdminLaunchInstance

View File

@ -14,8 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from horizon.dashboards.nova.instances_and_volumes.instances.workflows import (
LaunchInstance)
from horizon.dashboards.nova.instances.workflows import LaunchInstance
class AdminLaunchInstance(LaunchInstance):

View File

@ -0,0 +1,14 @@
from django.utils.translation import ugettext_lazy as _
import horizon
from horizon.dashboards.syspanel import dashboard
class Volumes(horizon.Panel):
name = _("Volumes")
slug = "volumes"
services = ('volume',)
dashboard.Syspanel.register(Volumes)

View File

@ -0,0 +1,19 @@
from django.utils.translation import ugettext_lazy as _
from horizon import tables
from horizon.dashboards.nova.volumes.tables import (UpdateRow,
VolumesTable as _VolumesTable, DeleteVolume)
class VolumesTable(_VolumesTable):
name = tables.Column("display_name",
verbose_name=_("Name"),
link="horizon:syspanel:volumes:detail")
class Meta:
name = "volumes"
verbose_name = _("Volumes")
status_columns = ["status"]
row_class = UpdateRow
table_actions = (DeleteVolume,)
row_actions = (DeleteVolume,)

View File

@ -0,0 +1,15 @@
{% extends 'syspanel/base.html' %}
{% load i18n %}
{% block title %}{% trans "Volume Details" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Volume Detail") %}
{% endblock page_header %}
{% block syspanel_main %}
<div class="row-fluid">
<div class="span12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}

View File

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

View File

@ -0,0 +1,38 @@
# 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 import http
from django.core.urlresolvers import reverse
from mox import IsA
from horizon import api
from horizon import test
class VolumeTests(test.BaseAdminViewTests):
@test.create_stubs({api: ('server_list', 'volume_list',)})
def test_index(self):
api.volume_list(IsA(http.HttpRequest)).AndReturn(self.volumes.list())
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list())
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:syspanel:volumes:index'))
self.assertTemplateUsed(res, 'syspanel/volumes/index.html')
volumes = res.context['volumes_table'].data
self.assertItemsEqual(volumes, self.volumes.list())

View File

@ -0,0 +1,8 @@
from django.conf.urls.defaults import patterns, url
from .views import IndexView, DetailView
urlpatterns = patterns('',
url(r'^$', IndexView.as_view(), name='index'),
url(r'^(?P<volume_id>[^/]+)/$', DetailView.as_view(), name='detail'),
)

View File

@ -0,0 +1,32 @@
# 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.
"""
Admin views for managing Nova volumes.
"""
from horizon.dashboards.nova.volumes.views import (IndexView as _IndexView,
DetailView as _DetailView)
from .tables import VolumesTable
class IndexView(_IndexView):
table_class = VolumesTable
template_name = "syspanel/volumes/index.html"
class DetailView(_DetailView):
template_name = "syspanel/volumes/detail.html"

View File

@ -44,7 +44,7 @@ class GlobalUsageTable(BaseUsageTable):
def get_instance_link(datum):
view = "horizon:nova:instances_and_volumes:instances:detail"
view = "horizon:nova:instances:detail"
if datum.get('instance_id', False):
return urlresolvers.reverse(view, args=(datum.get('instance_id'),))
else:

View File

@ -64,6 +64,7 @@ class ActionMetaclass(forms.forms.DeclarativeFieldsMetaclass):
attrs['name'] = getattr(opts, "name", name)
attrs['slug'] = getattr(opts, "slug", slugify(name))
attrs['roles'] = getattr(opts, "roles", ())
attrs['services'] = getattr(opts, "services", ())
attrs['progress_message'] = getattr(opts,
"progress_message",
_("Processing..."))
@ -112,6 +113,11 @@ class Action(forms.Form):
A list of role names which this action requires in order to be
completed. Defaults to an empty list (``[]``).
.. attribute:: services
A list of service types which this action requires in order to be
completed. Defaults to an empty list (``[]``).
.. attribute:: help_text
A string of simple help text to be displayed alongside the Action's
@ -257,6 +263,10 @@ class Step(object):
.. attribute:: roles
Inherited from the ``Action`` class.
.. attribute:: services
Inherited from the ``Action`` class.
"""
action_class = None
depends_on = ()
@ -284,6 +294,7 @@ class Step(object):
self.slug = self.action_class.slug
self.name = self.action_class.name
self.roles = self.action_class.roles
self.services = self.action_class.services
self.has_errors = False
self._handlers = {}