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:
parent
010475bbcd
commit
2c0a8f3220
@ -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:
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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__)
|
||||
|
@ -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)
|
||||
|
@ -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')
|
@ -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)
|
@ -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"))
|
@ -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):
|
@ -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" />
|
@ -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>
|
@ -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>
|
||||
{% endif %}
|
||||
<a class='btn btn-mini' href="{% url horizon:nova:instances:detail instance_id %}">{% trans "Reload" %}</a></p>
|
||||
{% endif %}
|
@ -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 %}
|
@ -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 %}
|
@ -7,5 +7,5 @@
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block dash_main %}
|
||||
{% include 'nova/instances_and_volumes/instances/_update.html' %}
|
||||
{% include 'nova/instances/_update.html' %}
|
||||
{% endblock %}
|
@ -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")
|
@ -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')
|
@ -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,
|
@ -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,
|
@ -1,17 +0,0 @@
|
||||
{% extends 'nova/base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}Instances & Volumes{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Instances & Volumes") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block dash_main %}
|
||||
<div id="instances">
|
||||
{{ instances_table.render }}
|
||||
</div>
|
||||
|
||||
<div id="volumes">
|
||||
{{ volumes_table.render }}
|
||||
</div>
|
||||
{% endblock %}
|
@ -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())
|
@ -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
|
@ -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):
|
@ -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)
|
@ -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):
|
@ -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)
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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>
|
@ -7,5 +7,5 @@
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block dash_main %}
|
||||
{% include 'nova/instances_and_volumes/volumes/_attach.html' %}
|
||||
{% include 'nova/volumes/_attach.html' %}
|
||||
{% endblock %}
|
@ -7,5 +7,5 @@
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block dash_main %}
|
||||
{% include 'nova/instances_and_volumes/volumes/_create.html' %}
|
||||
{% include 'nova/volumes/_create.html' %}
|
||||
{% endblock %}
|
@ -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 %}
|
11
horizon/dashboards/nova/volumes/templates/volumes/index.html
Normal file
11
horizon/dashboards/nova/volumes/templates/volumes/index.html
Normal 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 %}
|
@ -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()
|
@ -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'),
|
||||
)
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
14
horizon/dashboards/syspanel/volumes/panel.py
Normal file
14
horizon/dashboards/syspanel/volumes/panel.py
Normal 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)
|
19
horizon/dashboards/syspanel/volumes/tables.py
Normal file
19
horizon/dashboards/syspanel/volumes/tables.py
Normal 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,)
|
@ -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 %}
|
@ -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 %}
|
||||
|
||||
|
38
horizon/dashboards/syspanel/volumes/tests.py
Normal file
38
horizon/dashboards/syspanel/volumes/tests.py
Normal 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())
|
8
horizon/dashboards/syspanel/volumes/urls.py
Normal file
8
horizon/dashboards/syspanel/volumes/urls.py
Normal 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'),
|
||||
)
|
32
horizon/dashboards/syspanel/volumes/views.py
Normal file
32
horizon/dashboards/syspanel/volumes/views.py
Normal 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"
|
@ -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:
|
||||
|
@ -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 = {}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user