Converts instances and volumes to new tables, modals, etc.

This commit reworks the instances and volumes panels, extends that to
the syspanel instances panel, cleans up usage-related code and
moves it to overview and/or tenants panels as appropriate,
and finally implements a new layout/modal interface style
for combined modal/table views like security groups and
volume attachments.

Re-ordered the attach volume form. Fixed bug 913863.
Reworked syspanel usage views. Fixed bug 904861.
Table displays have much more useful data. Fixed bug 905065 and fixed bug 907512.
New modals fixed bug 898867.

Lots of additional code cleanup and fixes.

Change-Id: I407d3ec70a080883c137a963fa0ee22124b53dc2
This commit is contained in:
Gabriel Hurley 2012-01-12 14:47:10 -08:00
parent 29b70fbf92
commit 6c359166a6
65 changed files with 1391 additions and 1755 deletions
horizon/horizon
openstack-dashboard/dashboard/static/dashboard/css

@ -25,6 +25,7 @@ import logging
from django.contrib import messages
from novaclient.v1_1 import client as nova_client
from novaclient.v1_1 import security_group_rules as nova_rules
from novaclient.v1_1.servers import REBOOT_HARD
from horizon.api.base import *
@ -87,7 +88,8 @@ class Server(APIResourceWrapper):
"""
_attrs = ['addresses', 'attrs', 'hostId', 'id', 'image', 'links',
'metadata', 'name', 'private_ip', 'public_ip', 'status', 'uuid',
'image_name', 'VirtualInterfaces', 'flavor', 'key_name']
'image_name', 'VirtualInterfaces', 'flavor', 'key_name',
'OS-EXT-STS:power_state', 'OS-EXT-STS:task_state']
def __init__(self, apiresource, request):
super(Server, self).__init__(apiresource)
@ -135,12 +137,31 @@ class Usage(APIResourceWrapper):
class SecurityGroup(APIResourceWrapper):
"""Simple wrapper around openstackx.extras.security_groups.SecurityGroup"""
_attrs = ['id', 'name', 'description', 'tenant_id', 'rules']
_attrs = ['id', 'name', 'description', 'tenant_id']
@property
def rules(self):
""" Wraps transmitted rule info in the novaclient rule class. """
if not hasattr(self, "_rules"):
manager = nova_rules.SecurityGroupRuleManager
self._rules = [nova_rules.SecurityGroupRule(manager, rule) for \
rule in self._apiresource.rules]
return self._rules
@rules.setter
def rules(self, value):
self._rules = value
class SecurityGroupRule(APIDictWrapper):
class SecurityGroupRule(APIResourceWrapper):
""" Simple wrapper for individual rules in a SecurityGroup. """
_attrs = ['ip_protocol', 'from_port', 'to_port', 'ip_range']
_attrs = ['id', 'ip_protocol', 'from_port', 'to_port', 'ip_range']
def __unicode__(self):
vals = {'from': self.from_port,
'to': self.to_port,
'cidr': self.ip_range['cidr']}
return 'ALLOW %(from)s:%(to)s from %(cidr)s' % vals
def novaclient(request):
@ -192,6 +213,7 @@ def floating_ip_pools_list(request):
return [FloatingIpPool(pool)
for pool in novaclient(request).floating_ip_pools.list()]
def tenant_floating_ip_get(request, floating_ip_id):
"""
Fetches a floating ip.
@ -348,13 +370,13 @@ def security_group_delete(request, security_group_id):
def security_group_rule_create(request, parent_group_id, ip_protocol=None,
from_port=None, to_port=None, cidr=None,
group_id=None):
return SecurityGroup(novaclient(request).\
security_group_rules.create(parent_group_id,
ip_protocol,
from_port,
to_port,
cidr,
group_id))
return SecurityGroupRule(novaclient(request).\
security_group_rules.create(parent_group_id,
ip_protocol,
from_port,
to_port,
cidr,
group_id))
def security_group_rule_delete(request, security_group_rule_id):

@ -81,9 +81,9 @@ class AddRule(forms.SelfHandlingForm):
data['to_port'],
data['cidr'])
messages.success(request, _('Successfully added rule: %s') \
% rule.id)
% unicode(rule))
except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in AddRule")
messages.error(request, _('Error adding rule security group: %s')
% e.message)
return shortcuts.redirect(request.build_absolute_uri())
return shortcuts.redirect("horizon:nova:access_and_security:index")

@ -52,6 +52,7 @@ class EditRules(tables.LinkAction):
name = "edit_rules"
verbose_name = _("Edit Rules")
url = "horizon:nova:access_and_security:security_groups:edit_rules"
attrs = {"class": "ajax-modal"}
class SecurityGroupsTable(tables.DataTable):
@ -69,19 +70,24 @@ class SecurityGroupsTable(tables.DataTable):
class DeleteRule(tables.DeleteAction):
data_type_singular = _("Security Group Rule")
data_type_plural = _("Security Group Rules")
data_type_singular = _("Rule")
data_type_plural = _("Rules")
def delete(self, request, obj_id):
api.security_group_rule_delete(request, obj_id)
def get_success_url(self, request):
return reverse("horizon:nova:access_and_security:index")
def get_cidr(rule):
return rule.ip_range['cidr']
class RulesTable(tables.DataTable):
protocol = tables.Column("ip_protocol", verbose_name=_("IP Protocol"))
protocol = tables.Column("ip_protocol",
verbose_name=_("IP Protocol"),
filters=(unicode.upper,))
from_port = tables.Column("from_port", verbose_name=_("From Port"))
to_port = tables.Column("to_port", verbose_name=_("To Port"))
cidr = tables.Column(get_cidr, verbose_name=_("CIDR"))
@ -89,12 +95,11 @@ class RulesTable(tables.DataTable):
def sanitize_id(self, obj_id):
return int(obj_id)
def get_object_display(self, datum):
#FIXME (PaulM) Do something prettier here
return ', '.join([':'.join((k, str(v))) for
k, v in datum._apidict.iteritems()])
def get_object_display(self, rule):
return unicode(rule)
class Meta:
name = "rules"
verbose_name = _("Security Group Rules")
table_actions = (DeleteRule,)
row_actions = (DeleteRule,)

@ -24,6 +24,7 @@ from django.core.urlresolvers import reverse
from glance.common import exception as glance_exception
from openstackx.api import exceptions as api_exceptions
from novaclient import exceptions as novaclient_exceptions
from novaclient.v1_1 import security_group_rules as nova_rules
from mox import IgnoreArg, IsA
from horizon import api
@ -56,12 +57,15 @@ class SecurityGroupsViewTests(test.BaseViewTests):
sg2.name = 'group_2'
rule = {'id': 1,
'ip_protocol': "tcp",
'ip_protocol': u"tcp",
'from_port': "80",
'to_port': "80",
'parent_group_id': "2",
'ip_range': {'cidr': "0.0.0.0/32"}}
self.rules = [api.nova.SecurityGroupRule(rule)]
manager = nova_rules.SecurityGroupRuleManager
rule_obj = nova_rules.SecurityGroupRule(manager, rule)
self.rules = [rule_obj]
sg1.rules = self.rules
sg2.rules = self.rules
self.security_groups = (sg1, sg2)
@ -179,7 +183,7 @@ class SecurityGroupsViewTests(test.BaseViewTests):
res = self.client.post(SG_EDIT_RULE_URL, formData)
self.assertRedirectsNoFollow(res, SG_EDIT_RULE_URL)
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_edit_rules_add_rule_exception(self):
exception = novaclient_exceptions.ClientException('ClientException',
@ -208,7 +212,7 @@ class SecurityGroupsViewTests(test.BaseViewTests):
res = self.client.post(SG_EDIT_RULE_URL, formData)
self.assertRedirectsNoFollow(res, SG_EDIT_RULE_URL)
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_edit_rules_delete_rule(self):
RULE_ID = 1
@ -224,7 +228,7 @@ class SecurityGroupsViewTests(test.BaseViewTests):
handled = table.maybe_handle()
self.assertEqual(strip_absolute_base(handled['location']),
SG_EDIT_RULE_URL)
INDEX_URL)
def test_edit_rules_delete_rule_exception(self):
RULE_ID = 1
@ -244,7 +248,7 @@ class SecurityGroupsViewTests(test.BaseViewTests):
handled = table.maybe_handle()
self.assertEqual(strip_absolute_base(handled['location']),
SG_EDIT_RULE_URL)
INDEX_URL)
def test_delete_group(self):
self.mox.StubOutWithMock(api, 'security_group_delete')

@ -73,6 +73,10 @@ class EditRulesView(tables.DataTableView):
context = self.get_context_data(**kwargs)
context['form'] = form
context['security_group'] = self.object
if request.is_ajax():
context['hide'] = True
self.template_name = ('nova/access_and_security/security_groups'
'/_edit_rules.html')
return self.render_to_response(context)
def post(self, request, *args, **kwargs):

@ -164,7 +164,7 @@ class LaunchForm(forms.SelfHandlingForm):
LOG.info(msg)
messages.success(request, msg)
return redirect(
'horizon:nova:instances_and_volumes:instances:index')
'horizon:nova:instances_and_volumes:index')
except:
exceptions.handle(request, _('Unable to launch instance.'))

@ -29,7 +29,7 @@ from horizon import api
from horizon import test
IMAGES_INDEX_URL = reverse('horizon:nova:images_and_snapshots:images:index')
IMAGES_INDEX_URL = reverse('horizon:nova:images_and_snapshots:index')
class FakeQuota:
@ -169,7 +169,7 @@ class ImageViewTests(test.BaseViewTests):
form_data)
self.assertRedirectsNoFollow(res,
reverse('horizon:nova:instances_and_volumes:instances:index'))
reverse('horizon:nova:instances_and_volumes:index'))
def test_launch_flavorlist_error(self):
IMAGE_ID = '1'

@ -49,8 +49,8 @@ class CreateSnapshot(forms.SelfHandlingForm):
messages.info(request,
_('Snapshot "%(name)s" created for instance "%(inst)s"') %
{"name": data['name'], "inst": instance.name})
return shortcuts.redirect('horizon:nova:images_and_snapshots'
':snapshots:index')
return shortcuts.redirect('horizon:nova:images_and_snapshots:'
'index')
except api_exceptions.ApiException, e:
msg = _('Error Creating Snapshot: %s') % e.message
LOG.exception(msg)

@ -91,7 +91,7 @@ class SnapshotsViewTests(test.BaseViewTests):
args=[self.bad_server.id]))
self.assertRedirectsNoFollow(res,
reverse('horizon:nova:instances_and_volumes:instances:index'))
reverse('horizon:nova:instances_and_volumes:index'))
def test_create_get_server_exception(self):
self.mox.StubOutWithMock(api, 'server_get')
@ -106,7 +106,7 @@ class SnapshotsViewTests(test.BaseViewTests):
args=[self.good_server.id]))
self.assertRedirectsNoFollow(res,
reverse('horizon:nova:instances_and_volumes:instances:index'))
reverse('horizon:nova:instances_and_volumes:index'))
def test_create_snapshot_post(self):
SNAPSHOT_NAME = 'snappy'
@ -136,7 +136,7 @@ class SnapshotsViewTests(test.BaseViewTests):
formData)
self.assertRedirectsNoFollow(res,
reverse('horizon:nova:images_and_snapshots:snapshots:index'))
reverse('horizon:nova:images_and_snapshots:index'))
def test_create_snapshot_post_exception(self):
SNAPSHOT_NAME = 'snappy'

@ -76,7 +76,7 @@ def create(request, instance_id):
LOG.exception(msg)
messages.error(request, msg)
return shortcuts.redirect(
'horizon:nova:instances_and_volumes:instances:index')
'horizon:nova:instances_and_volumes:index')
valid_states = ['ACTIVE']
if instance.status not in valid_states:
@ -84,7 +84,7 @@ def create(request, instance_id):
one of the following: %s") %
', '.join(valid_states))
return shortcuts.redirect(
'horizon:nova:instances_and_volumes:instances:index')
'horizon:nova:instances_and_volumes:index')
return shortcuts.render(request,
'nova/images_and_snapshots/snapshots/create.html',

@ -26,145 +26,13 @@ from django.utils.translation import ugettext as _
import openstackx.api.exceptions as api_exceptions
from horizon import api
from horizon import exceptions
from horizon import forms
LOG = logging.getLogger(__name__)
class TerminateInstance(forms.SelfHandlingForm):
instance = forms.CharField(required=True)
def handle(self, request, data):
instance_id = data['instance']
instance = api.server_get(request, instance_id)
try:
api.server_delete(request, instance)
except api_exceptions.ApiException, e:
LOG.exception(_('ApiException while terminating instance "%s"') %
instance_id)
messages.error(request,
_('Unable to terminate %(inst)s: %(message)s') %
{"inst": instance_id, "message": e.message})
else:
msg = _('Instance %s has been terminated.') % instance_id
LOG.info(msg)
messages.success(request, msg)
return shortcuts.redirect(request.build_absolute_uri())
class PauseInstance(forms.SelfHandlingForm):
instance = forms.CharField(required=True)
def handle(self, request, data):
instance_id = data['instance']
try:
server = api.server_pause(request, instance_id)
messages.success(request, _("Instance pausing"))
except api_exceptions.ApiException, e:
LOG.exception(_('ApiException while pausing instance "%s"') %
instance_id)
messages.error(request,
_('Unable to pause instance: %s') % e.message)
else:
msg = _('Instance %s has been paused.') % instance_id
LOG.info(msg)
messages.success(request, msg)
return shortcuts.redirect(request.build_absolute_uri())
class UnpauseInstance(forms.SelfHandlingForm):
instance = forms.CharField(required=True)
def handle(self, request, data):
instance_id = data['instance']
try:
server = api.server_unpause(request, instance_id)
messages.success(request, _("Instance unpausing"))
except api_exceptions.ApiException, e:
LOG.exception(_('ApiException while unpausing instance "%s"') %
instance_id)
messages.error(request,
_('Unable to unpause instance: %s') % e.message)
else:
msg = _('Instance %s has been unpaused.') % instance_id
LOG.info(msg)
messages.success(request, msg)
return shortcuts.redirect(request.build_absolute_uri())
class SuspendInstance(forms.SelfHandlingForm):
instance = forms.CharField(required=True)
def handle(self, request, data):
instance_id = data['instance']
try:
server = api.server_suspend(request, instance_id)
messages.success(request, _("Instance pausing"))
except api_exceptions.ApiException, e:
LOG.exception(_('ApiException while pausing instance "%s"') %
instance_id)
messages.error(request,
_('Unable to suspend instance: %s') % e.message)
else:
msg = _('Instance %s has been suspended.') % instance_id
LOG.info(msg)
messages.success(request, msg)
return shortcuts.redirect(request.build_absolute_uri())
class ResumeInstance(forms.SelfHandlingForm):
instance = forms.CharField(required=True)
def handle(self, request, data):
instance_id = data['instance']
try:
server = api.server_resume(request, instance_id)
messages.success(request, _("Instance resuming"))
except api_exceptions.ApiException, e:
LOG.exception(_('ApiException while resuming instance "%s"') %
instance_id)
messages.error(request,
_('Unable to resuming instance: %s') % e.message)
else:
msg = _('Instance %s has been resumed.') % instance_id
LOG.info(msg)
messages.success(request, msg)
return shortcuts.redirect(request.build_absolute_uri())
class RebootInstance(forms.SelfHandlingForm):
instance = forms.CharField(required=True)
def handle(self, request, data):
instance_id = data['instance']
try:
server = api.server_reboot(request, instance_id)
messages.success(request, _("Instance rebooting"))
except api_exceptions.ApiException, e:
LOG.exception(_('ApiException while rebooting instance "%s"') %
instance_id)
messages.error(request,
_('Unable to reboot instance: %s') % e.message)
else:
msg = _('Instance %s has been rebooted.') % instance_id
LOG.info(msg)
messages.success(request, msg)
return shortcuts.redirect(request.build_absolute_uri())
class UpdateInstance(forms.SelfHandlingForm):
tenant_id = forms.CharField(widget=forms.HiddenInput())
instance = forms.CharField(widget=forms.TextInput(
@ -174,14 +42,11 @@ class UpdateInstance(forms.SelfHandlingForm):
def handle(self, request, data):
tenant_id = data['tenant_id']
try:
api.server_update(request,
data['instance'],
data['name'])
messages.success(request, _("Instance '%s' updated") %
data['name'])
except api_exceptions.ApiException, e:
messages.error(request,
_('Unable to update instance: %s') % e.message)
api.server_update(request, data['instance'], data['name'])
messages.success(request,
_('Instance "%s" updated.') % data['name'])
except:
exceptions.handle(request, _('Unable to update instance.'))
return shortcuts.redirect(
'horizon:nova:instances_and_volumes:instances:index')
'horizon:nova:instances_and_volumes:index')

@ -0,0 +1,202 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from django import shortcuts
from django import template
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.template.defaultfilters import title
from novaclient import exceptions as novaclient_exceptions
from horizon import api
from horizon import tables
from horizon.templatetags import sizeformat
LOG = logging.getLogger(__name__)
ACTIVE_STATES = ("ACTIVE",)
POWER_STATES = {
0: "NO STATE",
1: "RUNNING",
2: "BLOCKED",
3: "PAUSED",
4: "SHUTDOWN",
5: "SHUTOFF",
6: "CRASHED",
7: "SUSPENDED",
8: "FAILED",
9: "BUILDING",
}
class TerminateInstance(tables.BatchAction):
name = "terminate"
action_present = _("Terminate")
action_past = _("Terminated")
data_type_singular = _("Instance")
data_type_plural = _("Instances")
classes = ('danger',)
def action(self, request, obj_id):
api.server_delete(request, obj_id)
class RebootInstance(tables.BatchAction):
name = "reboot"
action_present = _("Reboot")
action_past = _("Rebooted")
data_type_singular = _("Instance")
data_type_plural = _("Instances")
classes = ('danger',)
def allowed(self, request, instance=None):
return instance.status in ACTIVE_STATES
def action(self, request, obj_id):
api.server_reboot(request, obj_id)
class TogglePause(tables.BatchAction):
name = "pause"
action_present = _("Pause")
action_past = _("Paused")
data_type_singular = _("Instance")
data_type_plural = _("Instances")
def allowed(self, request, instance=None):
if not instance:
return True
self.paused = instance.status == "PAUSED"
if self.paused:
self.action_present = _("Unpause")
self.action_past = _("Unpaused")
return instance.status in ACTIVE_STATES
def action(self, request, obj_id):
if getattr(self, 'paused', False):
api.server_pause(request, obj_id)
else:
api.server_unpause(request, obj_id)
class ToggleSuspend(tables.BatchAction):
name = "suspend"
action_present = _("Suspend")
action_past = _("Suspended")
data_type_singular = _("Instance")
data_type_plural = _("Instances")
def allowed(self, request, instance=None):
if not instance:
return True
self.suspended = instance.status == "SUSPENDED"
if self.suspended:
self.action_present = _("Resume")
self.action_past = _("Resumed")
return instance.status in ACTIVE_STATES
def action(self, request, obj_id):
if getattr(self, 'suspended', False):
api.server_suspend(request, obj_id)
else:
api.server_resume(request, obj_id)
class LaunchLink(tables.LinkAction):
name = "launch"
verbose_name = _("Launch Instance")
url = "horizon:nova:images_and_snapshots:index"
attrs = {"class": "btn small"}
class EditInstance(tables.LinkAction):
name = "edit"
verbose_name = _("Edit Instance")
url = "horizon:nova:instances_and_volumes:instances:update"
attrs = {"class": "ajax-modal"}
class SnapshotLink(tables.LinkAction):
name = "snapshot"
verbose_name = _("Snapshot")
url = "horizon:nova:images_and_snapshots:snapshots:create"
def allowed(self, request, instance=None):
return instance.status in ACTIVE_STATES
class ConsoleLink(tables.LinkAction):
name = "console"
verbose_name = _("VNC Console")
url = "horizon:nova:instances_and_volumes:instances:vnc"
def allowed(self, request, instance=None):
return instance.status in ACTIVE_STATES
class LogLink(tables.LinkAction):
name = "log"
verbose_name = _("View Log")
url = "horizon:nova:instances_and_volumes:instances:console"
def allowed(self, request, instance=None):
return instance.status in ACTIVE_STATES
def get_ips(instance):
template_name = 'nova/instances_and_volumes/instances/_instance_ips.html'
context = {"instance": instance}
return template.loader.render_to_string(template_name, context)
def get_size(instance):
if hasattr(instance, "full_flavor"):
size_string = _("%(RAM)s RAM | %(VCPU)s VCPU | %(disk)s Disk")
vals = {'RAM': sizeformat.mbformat(instance.full_flavor.ram),
'VCPU': instance.full_flavor.vcpus,
'disk': sizeformat.diskgbformat(instance.full_flavor.disk)}
return size_string % vals
return _("Not available")
def get_power_state(instance):
return POWER_STATES.get(getattr(instance, "OS-EXT-STS:power_state", 0), '')
class InstancesTable(tables.DataTable):
name = tables.Column("name", link="horizon:nova:instances_and_volumes:" \
"instances:detail")
ip = tables.Column(get_ips, verbose_name=_("IP Address"))
size = tables.Column(get_size, verbose_name=_("Size"))
status = tables.Column("status", filters=(title,))
task = tables.Column("OS-EXT-STS:task_state",
verbose_name=_("Task"),
filters=(title,))
state = tables.Column(get_power_state,
filters=(title,),
verbose_name=_("Power State"))
class Meta:
name = "instances"
verbose_name = _("Instances")
table_actions = (LaunchLink, TerminateInstance)
row_actions = (EditInstance, ConsoleLink, LogLink, SnapshotLink,
TogglePause, ToggleSuspend, RebootInstance,
TerminateInstance)

@ -24,208 +24,97 @@ from django import http
from django.contrib import messages
from django.core.urlresolvers import reverse
from mox import IsA, IgnoreArg
from openstackx.api import exceptions as api_exceptions
from novaclient import exceptions as nova_exceptions
from horizon import api
from horizon import test
INDEX_URL = reverse('horizon:nova:instances_and_volumes:index')
class InstanceViewTests(test.BaseViewTests):
def setUp(self):
super(InstanceViewTests, self).setUp()
self.now = self.override_times()
server = api.Server(None, self.request)
server.id = 1
server.id = "1"
server.name = 'serverName'
server.status = "ACTIVE"
volume = api.Volume(self.request)
volume.id = 1
volume.id = "1"
self.servers = (server,)
self.volumes = (volume,)
def test_terminate_instance(self):
formData = {'method': 'TerminateInstance',
'instance': self.servers[0].id,
}
def tearDown(self):
super(InstanceViewTests, self).tearDown()
self.reset_times()
self.mox.StubOutWithMock(api, 'server_get')
api.server_get(IsA(http.HttpRequest),
str(self.servers[0].id)).AndReturn(self.servers[0])
def test_terminate_instance(self):
self.mox.StubOutWithMock(api, 'server_list')
self.mox.StubOutWithMock(api, 'flavor_list')
self.mox.StubOutWithMock(api, 'server_delete')
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers)
api.flavor_list(IgnoreArg()).AndReturn([])
api.server_delete(IsA(http.HttpRequest),
self.servers[0])
self.servers[0].id)
self.mox.ReplayAll()
res = self.client.post(
reverse('horizon:nova:instances_and_volumes:instances:index'),
formData)
formData = {'action': 'instances__terminate__%s' % self.servers[0].id}
res = self.client.post(INDEX_URL, formData)
self.assertRedirectsNoFollow(res,
reverse('horizon:nova:instances_and_volumes:instances:index'))
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_terminate_instance_exception(self):
formData = {'method': 'TerminateInstance',
'instance': self.servers[0].id,
}
self.mox.StubOutWithMock(api, 'server_get')
api.server_get(IsA(http.HttpRequest),
str(self.servers[0].id)).AndReturn(self.servers[0])
exception = api_exceptions.ApiException('ApiException',
message='apiException')
self.mox.StubOutWithMock(api, 'server_list')
self.mox.StubOutWithMock(api, 'flavor_list')
self.mox.StubOutWithMock(api, 'server_delete')
api.server_delete(IsA(http.HttpRequest),
self.servers[0]).AndRaise(exception)
self.mox.StubOutWithMock(messages, 'error')
messages.error(IsA(http.HttpRequest), IsA(unicode))
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers)
api.flavor_list(IgnoreArg()).AndReturn([])
exception = nova_exceptions.ClientException(500)
api.server_delete(IsA(http.HttpRequest),
self.servers[0].id).AndRaise(exception)
self.mox.ReplayAll()
res = self.client.post(
reverse('horizon:nova:instances_and_volumes:instances:index'),
formData)
formData = {'action': 'instances__terminate__%s' % self.servers[0].id}
res = self.client.post(INDEX_URL, formData)
self.assertRedirectsNoFollow(res,
reverse('horizon:nova:instances_and_volumes:instances:index'))
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_reboot_instance(self):
formData = {'method': 'RebootInstance',
'instance': self.servers[0].id,
}
self.mox.StubOutWithMock(api, 'server_reboot')
self.mox.StubOutWithMock(api, 'server_list')
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers)
api.server_reboot(IsA(http.HttpRequest), unicode(self.servers[0].id))
self.mox.ReplayAll()
res = self.client.post(
reverse('horizon:nova:instances_and_volumes:instances:index'),
formData)
formData = {'action': 'instances__reboot__%s' % self.servers[0].id}
res = self.client.post(INDEX_URL, formData)
self.assertRedirectsNoFollow(res,
reverse('horizon:nova:instances_and_volumes:instances:index'))
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_reboot_instance_exception(self):
formData = {'method': 'RebootInstance',
'instance': self.servers[0].id,
}
self.mox.StubOutWithMock(api, 'server_reboot')
exception = api_exceptions.ApiException('ApiException',
message='apiException')
self.mox.StubOutWithMock(api, 'server_list')
api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers)
exception = nova_exceptions.ClientException(500)
api.server_reboot(IsA(http.HttpRequest),
unicode(self.servers[0].id)).AndRaise(exception)
self.mox.StubOutWithMock(messages, 'error')
messages.error(IsA(http.HttpRequest), IsA(basestring))
self.mox.ReplayAll()
res = self.client.post(
reverse('horizon:nova:instances_and_volumes:instances:index'),
formData)
formData = {'action': 'instances__reboot__%s' % self.servers[0].id}
res = self.client.post(INDEX_URL, formData)
self.assertRedirectsNoFollow(res,
reverse('horizon:nova:instances_and_volumes:instances:index'))
def test_instance_usage(self):
TEST_RETURN = 'testReturn'
now = self.override_times()
self.mox.StubOutWithMock(api, 'usage_get')
api.usage_get(IsA(http.HttpRequest), self.TEST_TENANT,
datetime.datetime(now.year, now.month, 1,
now.hour, now.minute, now.second),
now).AndReturn(TEST_RETURN)
self.mox.ReplayAll()
res = self.client.get(
reverse('horizon:nova:instances_and_volumes:instances:usage'))
self.assertTemplateUsed(res,
'nova/instances_and_volumes/instances/usage.html')
self.assertEqual(res.context['usage'], TEST_RETURN)
self.reset_times()
def test_instance_csv_usage(self):
TEST_RETURN = 'testReturn'
now = self.override_times()
self.mox.StubOutWithMock(api, 'usage_get')
api.usage_get(IsA(http.HttpRequest), self.TEST_TENANT,
datetime.datetime(now.year, now.month, 1,
now.hour, now.minute, now.second),
now).AndReturn(TEST_RETURN)
self.mox.ReplayAll()
res = self.client.get(
reverse('horizon:nova:instances_and_volumes:instances:usage') +
"?format=csv")
self.assertTemplateUsed(res,
'nova/instances_and_volumes/instances/usage.csv')
self.assertEqual(res.context['usage'], TEST_RETURN)
self.reset_times()
def test_instance_usage_exception(self):
now = self.override_times()
exception = api_exceptions.ApiException('apiException',
message='apiException')
self.mox.StubOutWithMock(api, 'usage_get')
api.usage_get(IsA(http.HttpRequest), self.TEST_TENANT,
datetime.datetime(now.year, now.month, 1,
now.hour, now.minute, now.second),
now).AndRaise(exception)
self.mox.StubOutWithMock(messages, 'error')
messages.error(IsA(http.HttpRequest), IsA(basestring))
self.mox.ReplayAll()
res = self.client.get(
reverse('horizon:nova:instances_and_volumes:instances:usage'))
self.assertTemplateUsed(res,
'nova/instances_and_volumes/instances/usage.html')
self.assertEqual(res.context['usage'], {})
self.reset_times()
def test_instance_usage_default_tenant(self):
TEST_RETURN = 'testReturn'
now = self.override_times()
self.mox.StubOutWithMock(api, 'usage_get')
api.usage_get(IsA(http.HttpRequest), self.TEST_TENANT,
datetime.datetime(now.year, now.month, 1,
now.hour, now.minute, now.second),
now).AndReturn(TEST_RETURN)
self.mox.ReplayAll()
res = self.client.get(
reverse('horizon:nova:instances_and_volumes:instances:usage'))
self.assertTemplateUsed(res,
'nova/instances_and_volumes/instances/usage.html')
self.assertEqual(res.context['usage'], TEST_RETURN)
self.reset_times()
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_instance_console(self):
CONSOLE_OUTPUT = 'output'
@ -272,8 +161,7 @@ class InstanceViewTests(test.BaseViewTests):
def test_instance_vnc_exception(self):
INSTANCE_ID = self.servers[0].id
exception = api_exceptions.ApiException('apiException',
message='apiException')
exception = nova_exceptions.ClientException(500)
self.mox.StubOutWithMock(api, 'console_create')
api.console_create(IsA(http.HttpRequest),
@ -286,8 +174,7 @@ class InstanceViewTests(test.BaseViewTests):
reverse('horizon:nova:instances_and_volumes:instances:vnc',
args=[INSTANCE_ID]))
self.assertRedirectsNoFollow(res,
reverse('horizon:nova:instances_and_volumes:instances:index'))
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_instance_update_get(self):
INSTANCE_ID = self.servers[0].id
@ -308,7 +195,7 @@ class InstanceViewTests(test.BaseViewTests):
def test_instance_update_get_server_get_exception(self):
INSTANCE_ID = self.servers[0].id
exception = api_exceptions.ApiException('apiException')
exception = nova_exceptions.ClientException(500)
self.mox.StubOutWithMock(api, 'server_get')
api.server_get(IsA(http.HttpRequest),
unicode(INSTANCE_ID)).AndRaise(exception)
@ -319,8 +206,7 @@ class InstanceViewTests(test.BaseViewTests):
reverse('horizon:nova:instances_and_volumes:instances:update',
args=[INSTANCE_ID]))
self.assertRedirectsNoFollow(res,
reverse('horizon:nova:instances_and_volumes:instances:index'))
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_instance_update_post(self):
INSTANCE_ID = self.servers[0].id
@ -344,32 +230,28 @@ class InstanceViewTests(test.BaseViewTests):
reverse('horizon:nova:instances_and_volumes:instances:update',
args=[INSTANCE_ID]), formData)
self.assertRedirectsNoFollow(res,
reverse('horizon:nova:instances_and_volumes:instances:index'))
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_instance_update_post_api_exception(self):
INSTANCE_ID = self.servers[0].id
NAME = 'myname'
formData = {'method': 'UpdateInstance',
'instance': INSTANCE_ID,
'name': NAME,
'tenant_id': self.TEST_TENANT}
SERVER = self.servers[0]
self.mox.StubOutWithMock(api, 'server_get')
api.server_get(IsA(http.HttpRequest),
unicode(INSTANCE_ID)).AndReturn(self.servers[0])
exception = api_exceptions.ApiException('apiException')
self.mox.StubOutWithMock(api, 'server_update')
api.server_update(IsA(http.HttpRequest),
str(INSTANCE_ID), NAME).\
AndRaise(exception)
api.server_get(IsA(http.HttpRequest), unicode(SERVER.id)) \
.AndReturn(self.servers[0])
exception = nova_exceptions.ClientException(500)
api.server_update(IsA(http.HttpRequest), str(SERVER.id), SERVER.name) \
.AndRaise(exception)
self.mox.ReplayAll()
res = self.client.post(
reverse('horizon:nova:instances_and_volumes:instances:update',
args=[INSTANCE_ID]), formData)
formData = {'method': 'UpdateInstance',
'instance': SERVER.id,
'name': SERVER.name,
'tenant_id': self.TEST_TENANT}
url = reverse('horizon:nova:instances_and_volumes:instances:update',
args=[SERVER.id])
res = self.client.post(url, formData)
self.assertRedirectsNoFollow(res,
reverse('horizon:nova:instances_and_volumes:instances:index'))
self.assertRedirectsNoFollow(res, INDEX_URL)

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

@ -27,174 +27,23 @@ import logging
from django import http
from django import shortcuts
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext as _
import openstackx.api.exceptions as api_exceptions
import horizon
from horizon import api
from horizon import exceptions
from horizon import forms
from horizon import test
from horizon.dashboards.nova.instances_and_volumes.instances.forms import (
TerminateInstance, PauseInstance, UnpauseInstance, SuspendInstance,
ResumeInstance, RebootInstance, UpdateInstance)
from horizon import views
from .forms import UpdateInstance
LOG = logging.getLogger(__name__)
def index(request):
tenant_id = request.user.tenant_id
for f in (TerminateInstance, RebootInstance):
form, handled = f.maybe_handle(request)
if handled:
return handled
instances = []
try:
instances = api.server_list(request)
except Exception as e:
LOG.exception(_('Exception in instance index'))
if not hasattr(e, 'message'):
e.message = str(e)
messages.error(request, _('Unable to get instance list: %s')
% e.message)
# Gather our flavors and correlate our instances to them
try:
flavors = api.flavor_list(request)
full_flavors = SortedDict([(str(flavor.id), flavor) for \
flavor in flavors])
for instance in instances:
instance.full_flavor = full_flavors[instance.flavor["id"]]
except api_exceptions.Unauthorized, e:
LOG.exception('Unauthorized attempt to access flavor list.')
messages.error(request, _('Unauthorized.'))
except Exception, e:
LOG.exception('Exception while fetching flavor info')
if not hasattr(e, 'message'):
e.message = str(e)
messages.error(request, _('Unable to get flavor info: %s') % e.message)
# We don't have any way of showing errors for these, so don't bother
# trying to reuse the forms from above
terminate_form = TerminateInstance()
pause_form = PauseInstance()
unpause_form = UnpauseInstance()
suspend_form = SuspendInstance()
resume_form = ResumeInstance()
reboot_form = RebootInstance()
return shortcuts.render(request,
'nova/instances_and_volumes/instances/index.html', {
'instances': instances,
'terminate_form': terminate_form,
'pause_form': pause_form,
'unpause_form': unpause_form,
'suspend_form': suspend_form,
'resume_form': resume_form,
'reboot_form': reboot_form})
def refresh(request):
tenant_id = request.user.tenant_id
instances = []
try:
instances = api.server_list(request)
except Exception as e:
if not hasattr(e, 'message'):
e.message = str(e)
messages.error(request,
_('Unable to get instance list: %s') % e.message)
# We don't have any way of showing errors for these, so don't bother
# trying to reuse the forms from above
terminate_form = TerminateInstance()
pause_form = PauseInstance()
unpause_form = UnpauseInstance()
suspend_form = SuspendInstance()
resume_form = ResumeInstance()
reboot_form = RebootInstance()
return shortcuts.render(request,
'nova/instances_and_volumes/instances/_list.html', {
'instances': instances,
'terminate_form': terminate_form,
'pause_form': pause_form,
'unpause_form': unpause_form,
'suspend_form': suspend_form,
'resume_form': resume_form,
'reboot_form': reboot_form})
def usage(request, tenant_id=None):
tenant_id = tenant_id or request.user.tenant_id
today = test.today()
date_start = datetime.date(today.year, today.month, 1)
datetime_start = datetime.datetime.combine(date_start, test.time())
datetime_end = test.utcnow()
show_terminated = request.GET.get('show_terminated', False)
usage = {}
if not tenant_id:
tenant_id = request.user.tenant_id
try:
usage = api.usage_get(request, tenant_id, datetime_start, datetime_end)
except api_exceptions.ApiException, e:
LOG.exception(_('ApiException in instance usage'))
messages.error(request, _('Unable to get usage info: %s') % e.message)
ram_unit = "MB"
total_ram = 0
if hasattr(usage, 'total_active_ram_size'):
total_ram = usage.total_active_ram_size
if total_ram > 999:
ram_unit = "GB"
total_ram /= float(1024)
running_instances = []
terminated_instances = []
if hasattr(usage, 'instances'):
now = datetime.datetime.now()
for i in usage.instances:
# this is just a way to phrase uptime in a way that is compatible
# with the 'timesince' filter. Use of local time intentional
i['uptime_at'] = now - datetime.timedelta(seconds=i['uptime'])
if i['ended_at']:
terminated_instances.append(i)
else:
running_instances.append(i)
instances = running_instances
if show_terminated:
instances += terminated_instances
if request.GET.get('format', 'html') == 'csv':
template_name = 'nova/instances_and_volumes/instances/usage.csv'
mimetype = "text/csv"
else:
template_name = 'nova/instances_and_volumes/instances/usage.html'
mimetype = "text/html"
dash_url = horizon.get_dashboard('nova').get_absolute_url()
return shortcuts.render(request, template_name, {
'usage': usage,
'ram_unit': ram_unit,
'total_ram': total_ram,
'csv_link': '?format=csv',
'show_terminated': show_terminated,
'datetime_start': datetime_start,
'datetime_end': datetime_end,
'instances': instances,
'dash_url': dash_url},
content_type=mimetype)
def console(request, instance_id):
tenant_id = request.user.tenant_id
try:
# TODO(jakedahn): clean this up once the api supports tailing.
length = request.GET.get('length', None)
@ -205,100 +54,81 @@ def console(request, instance_id):
response.write(console)
response.flush()
return response
except api_exceptions.ApiException, e:
LOG.exception(_('ApiException while fetching instance console'))
messages.error(request,
_('Unable to get log for instance %(inst)s: %(msg)s') %
{"inst": instance_id, "msg": e.message})
return shortcuts.redirect(
'horizon:nova:instances_and_volumes:instances:index')
except:
msg = _('Unable to get log for instance "%s".') % instance_id
redirect = reverse('horizon:nova:instances_and_volumes:index')
exceptions.handle(request, msg, redirect=redirect)
def vnc(request, instance_id):
tenant_id = request.user.tenant_id
try:
console = api.console_create(request, instance_id, 'vnc')
instance = api.server_get(request, instance_id)
return shortcuts.redirect(console.output +
("&title=%s(%s)" % (instance.name, instance_id)))
except api_exceptions.ApiException, e:
LOG.exception(_('ApiException while fetching instance vnc connection'))
messages.error(request,
_('Unable to get vnc console for instance %(inst)s: %(message)s') %
{"inst": instance_id, "message": e.message})
return shortcuts.redirect(
'horizon:nova:instances_and_volumes:instances:index')
except:
redirect = reverse("horizon:nova:instances_and_volumes:index")
msg = _('Unable to get VNC console for instance "%s".') % instance_id
exceptions.handle(request, msg, redirect=redirect)
def update(request, instance_id):
tenant_id = request.user.tenant_id
try:
instance = api.server_get(request, instance_id)
except api_exceptions.ApiException, e:
LOG.exception(_('ApiException while fetching instance info'))
messages.error(request,
_('Unable to get information for instance %(inst)s: %(message)s') %
{"inst": instance_id, "message": e.message})
return shortcuts.redirect(
'horizon:nova:instances_and_volumes:instances:index')
class UpdateView(forms.ModalFormView):
form_class = UpdateInstance
template_name = 'nova/instances_and_volumes/instances/update.html'
context_object_name = 'instance'
form, handled = UpdateInstance.maybe_handle(request, initial={
'instance': instance_id,
'tenant_id': tenant_id,
'name': instance.name})
def get_object(self, *args, **kwargs):
if not hasattr(self, "object"):
instance_id = self.kwargs['instance_id']
try:
self.object = api.server_get(self.request, instance_id)
except:
redirect = reverse("horizon:nova:instances_and_volumes:index")
msg = _('Unable to retrieve instance details.')
exceptions.handle(self.request, msg, redirect=redirect)
return self.object
if handled:
return handled
return shortcuts.render(request,
'nova/instances_and_volumes/instances/update.html', {
'instance': instance,
'form': form})
def get_initial(self):
return {'instance': self.kwargs['instance_id'],
'tenant_id': self.request.user.tenant_id,
'name': getattr(self.object, 'name', '')}
def detail(request, instance_id):
tenant_id = request.user.tenant_id
try:
instance = api.server_get(request, instance_id)
volumes = api.volume_instance_list(request, instance_id)
class DetailView(views.APIView):
template_name = 'nova/instances_and_volumes/instances/detail.html'
def get_data(self, request, context, *args, **kwargs):
instance_id = kwargs['instance_id']
try:
instance = api.server_get(request, instance_id)
volumes = api.volume_instance_list(request, instance_id)
except:
instance = None
redirect = reverse('horizon:nova:instances_and_volumes:index')
exceptions.handle(request,
_('Unable to retrieve details for '
'instance "%s".') % instance_id,
redirect=redirect)
try:
console = api.console_create(request, instance_id, 'vnc')
vnc_url = "%s&title=%s(%s)" % (console.output,
instance.name,
getattr(instance, "name", ""),
instance_id)
except api_exceptions.ApiException, e:
LOG.exception(_('ApiException while fetching instance vnc \
connection'))
messages.error(request,
_('Unable to get vnc console for instance %(inst)s: %(msg)s') %
{"inst": instance_id, "msg": e.message})
return shortcuts.redirect(
'horizon:nova:instances_and_volumes:instances:index')
except api_exceptions.ApiException, e:
LOG.exception(_('ApiException while fetching instance info'))
messages.error(request,
_('Unable to get information for instance %(inst)s: %(msg)s') %
{"inst": instance_id, "msg": e.message})
return shortcuts.redirect(
'horizon:nova:instances_and_volumes:instances:index')
except:
vnc_url = ""
exceptions.handle(request,
_('Unable to get vnc console for '
'instance "%s".') % instance_id)
# Gather our flavors and images and correlate our instances to them
try:
# Gather our flavors and images and correlate our instances to them
# Exception handling happens in the parent class.
flavors = api.flavor_list(request)
full_flavors = SortedDict([(str(flavor.id), flavor) for \
flavor in flavors])
instance.full_flavor = full_flavors[instance.flavor["id"]]
except api_exceptions.Unauthorized, e:
LOG.exception('Unauthorized attempt to access flavor list.')
messages.error(request, _('Unauthorized.'))
except Exception, e:
LOG.exception('Exception while fetching flavor info')
if not hasattr(e, 'message'):
e.message = str(e)
messages.error(request, _('Unable to get flavor info: %s') % e.message)
return shortcuts.render(request,
'nova/instances_and_volumes/instances/detail.html', {
'instance': instance,
'vnc_url': vnc_url,
'volumes': volumes})
context.update({'instance': instance,
'vnc_url': vnc_url,
'volumes': volumes})
return context

@ -36,9 +36,12 @@ class InstancesAndVolumesViewTest(test.BaseViewTests):
server = api.Server(None, self.request)
server.id = 1
server.name = 'serverName'
server.status = "ACTIVE"
volume = api.Volume(self.request)
volume.id = 1
volume.size = 10
volume.attachments = [{}]
self.servers = (server,)
self.volumes = (volume,)
@ -56,7 +59,8 @@ class InstancesAndVolumesViewTest(test.BaseViewTests):
self.assertTemplateUsed(res,
'nova/instances_and_volumes/index.html')
self.assertItemsEqual(res.context['instances'], self.servers)
instances = res.context['instances_table'].data
self.assertItemsEqual(instances, self.servers)
def test_index_server_list_exception(self):
self.mox.StubOutWithMock(api, 'server_list')
@ -72,4 +76,4 @@ class InstancesAndVolumesViewTest(test.BaseViewTests):
self.assertTemplateUsed(res,
'nova/instances_and_volumes/index.html')
self.assertEqual(len(res.context['instances']), 0)
self.assertEqual(len(res.context['instances_table'].data), 0)

@ -22,13 +22,12 @@ from django.conf.urls.defaults import *
import horizon
from horizon.dashboards.nova.instances_and_volumes.instances import urls\
as instance_urls
from horizon.dashboards.nova.instances_and_volumes.volumes import urls\
as volume_urls
from .instances import urls as instance_urls
from .views import IndexView
from .volumes import urls as volume_urls
urlpatterns = patterns('horizon.dashboards.nova.instances_and_volumes',
url(r'^$', 'views.index', name='index'),
url(r'', include(instance_urls, namespace='instances')),
url(r'', include(volume_urls, namespace='volumes')),
url(r'^$', IndexView.as_view(), name='index'),
url(r'^instances/', include(instance_urls, namespace='instances')),
url(r'^volumes/', include(volume_urls, namespace='volumes')),
)

@ -35,76 +35,45 @@ import openstackx.api.exceptions as api_exceptions
from horizon import api
from horizon import forms
from horizon import test
from horizon.dashboards.nova.instances_and_volumes.instances.forms import (
TerminateInstance, PauseInstance, UnpauseInstance, SuspendInstance,
ResumeInstance, RebootInstance, UpdateInstance)
from horizon.dashboards.nova.instances_and_volumes.volumes.forms import (
CreateForm, DeleteForm, AttachForm, DetachForm)
from horizon import tables
from .instances.tables import InstancesTable
from .volumes.tables import VolumesTable
LOG = logging.getLogger(__name__)
def index(request):
for f in (TerminateInstance, PauseInstance, UnpauseInstance,
SuspendInstance, ResumeInstance, RebootInstance,
DeleteForm, DetachForm):
form, handled = f.maybe_handle(request)
if handled:
return handled
class IndexView(tables.MultiTableView):
table_classes = (InstancesTable, VolumesTable)
template_name = 'nova/instances_and_volumes/index.html'
# Gather our instances
try:
instances = api.server_list(request)
except api_exceptions.ApiException as e:
instances = []
LOG.exception(_('Exception in instance index'))
messages.error(request, _('Unable to fetch instances: %s') % e.message)
def get_instances_data(self):
# Gather our instances
try:
instances = api.server_list(self.request)
except Exception as e:
instances = []
LOG.exception(_('Exception while fetching instances.'))
messages.error(self.request, _('Unable to retrieve instances.'))
# Gather our flavors and correlate our instances to them
try:
flavors = api.flavor_list(self.request)
full_flavors = SortedDict([(str(flavor.id), flavor) for \
flavor in flavors])
for instance in instances:
instance.full_flavor = full_flavors[instance.flavor["id"]]
except Exception, e:
LOG.exception('Exception while fetching flavor info.')
messages.error(self.request,
_('Unable to retrieve instance size information.'))
return instances
# Gather our volumes
try:
volumes = api.volume_list(request)
except novaclient_exceptions.ClientException, e:
volumes = []
LOG.exception("ClientException in volume index")
messages.error(request, _('Unable to fetch volumes: %s') % e.message)
# Gather our flavors and correlate our instances to them
try:
flavors = api.flavor_list(request)
full_flavors = SortedDict([(str(flavor.id), flavor) for \
flavor in flavors])
for instance in instances:
instance.full_flavor = full_flavors[instance.flavor["id"]]
except api_exceptions.Unauthorized, e:
LOG.exception('Unauthorized attempt to access flavor list.')
messages.error(request, _('Unauthorized.'))
except Exception, e:
if not hasattr(e, 'message'):
e.message = str(e)
LOG.exception('Exception while fetching flavor info')
messages.error(request, _('Unable to get flavor info: %s') % e.message)
terminate_form = TerminateInstance()
pause_form = PauseInstance()
unpause_form = UnpauseInstance()
suspend_form = SuspendInstance()
resume_form = ResumeInstance()
reboot_form = RebootInstance()
delete_form = DeleteForm()
detach_form = DetachForm()
create_form = CreateForm()
return shortcuts.render(request, 'nova/instances_and_volumes/index.html', {
'instances': instances,
'terminate_form': terminate_form,
'pause_form': pause_form,
'unpause_form': unpause_form,
'suspend_form': suspend_form,
'resume_form': resume_form,
'reboot_form': reboot_form,
'volumes': volumes,
'delete_form': delete_form,
'create_form': create_form,
'detach_form': detach_form})
def get_volumes_data(self):
# Gather our volumes
try:
volumes = api.volume_list(self.request)
except novaclient_exceptions.ClientException, e:
volumes = []
LOG.exception("ClientException in volume index")
messages.error(self.request, _('Unable to fetch volumes: %s') % e)
return volumes

@ -38,28 +38,13 @@ class CreateForm(forms.SelfHandlingForm):
LOG.exception("ClientException in CreateVolume")
messages.error(request,
_('Error Creating Volume: %s') % e.message)
return shortcuts.redirect(
"horizon:nova:instances_and_volumes:volumes:index")
class DeleteForm(forms.SelfHandlingForm):
volume_id = forms.CharField(widget=forms.HiddenInput())
volume_name = forms.CharField(widget=forms.HiddenInput())
def handle(self, request, data):
try:
api.volume_delete(request, data['volume_id'])
message = 'Deleting volume "%s"' % data['volume_id']
LOG.info(message)
messages.info(request, message)
except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in DeleteVolume")
messages.error(request,
_('Error deleting volume: %s') % e.message)
return shortcuts.redirect(request.build_absolute_uri())
return shortcuts.redirect("horizon:nova:instances_and_volumes:index")
class AttachForm(forms.SelfHandlingForm):
instance = forms.ChoiceField(label="Attach to Instance",
help_text=_("Select an instance to "
"attach to."))
device = forms.CharField(label="Device Name", initial="/dev/vdb")
def __init__(self, *args, **kwargs):
@ -75,10 +60,7 @@ class AttachForm(forms.SelfHandlingForm):
for instance in instance_list:
instances.append((instance.id, '%s (%s)' % (instance.name,
instance.id)))
self.fields['instance'] = forms.ChoiceField(
choices=instances,
label="Attach to Instance",
help_text="Select an instance to attach to.")
self.fields['instance'].choices = instances
def handle(self, request, data):
try:
@ -99,25 +81,4 @@ class AttachForm(forms.SelfHandlingForm):
messages.error(request,
_('Error attaching volume: %s') % e.message)
return shortcuts.redirect(
"horizon:nova:instances_and_volumes:volumes:index")
class DetachForm(forms.SelfHandlingForm):
volume_id = forms.CharField(widget=forms.HiddenInput())
instance_id = forms.CharField(widget=forms.HiddenInput())
attachment_id = forms.CharField(widget=forms.HiddenInput())
def handle(self, request, data):
try:
api.volume_detach(request, data['instance_id'],
data['attachment_id'])
message = (_('Detaching volume %(vol)s from instance %(inst)s') %
{"vol": data['volume_id'], "inst": data['instance_id']})
LOG.info(message)
messages.info(request, message)
except novaclient_exceptions.ClientException, e:
LOG.exception("ClientException in DetachVolume")
messages.error(request,
_('Error detaching volume: %s') % e.message)
return shortcuts.redirect(
"horizon:nova:instances_and_volumes:volumes:index")
"horizon:nova:instances_and_volumes:index")

@ -0,0 +1,141 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from django import shortcuts
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.template.defaultfilters import filesizeformat, title
from django.utils import safestring
from novaclient import exceptions as novaclient_exceptions
from horizon import api
from horizon import tables
LOG = logging.getLogger(__name__)
ACTIVE_STATES = ("ACTIVE",)
class DeleteVolume(tables.DeleteAction):
data_type_singular = _("Volume")
data_type_plural = _("Volumes")
classes = ('danger',)
def delete(self, request, obj_id):
api.volume_delete(request, obj_id)
class CreateVolume(tables.LinkAction):
name = "create"
verbose_name = _("Create Volume")
url = "horizon:nova:instances_and_volumes:volumes:create"
attrs = {"class": "btn small ajax-modal"}
class EditAttachments(tables.LinkAction):
name = "attachments"
verbose_name = _("Edit Attachments")
url = "horizon:nova:instances_and_volumes:volumes:attach"
def allowed(self, request, volume=None):
return volume.status in ("available", "in-use")
def get_size(volume):
return _("%s GB") % volume.size
def get_attachment(volume):
attachments = []
link = '<a href="%(url)s">Instance %(instance)s&nbsp;' \
'<small>(%(dev)s)</small></a>'
# Filter out "empty" attachments which the client returns...
for attachment in [att for att in volume.attachments if att]:
url = reverse("horizon:nova:instances_and_volumes:instances:detail",
args=(attachment["serverId"],))
# TODO(jake): Make "instance" the instance name
vals = {"url": url,
"instance": attachment["serverId"],
"dev": attachment["device"]}
attachments.append(link % vals)
return safestring.mark_safe(", ".join(attachments))
class VolumesTable(tables.DataTable):
name = tables.Column("displayName",
verbose_name=_("Name"),
link="horizon:nova:instances_and_volumes:"
"volumes:detail")
description = tables.Column("displayDescription",
verbose_name=("Description"))
size = tables.Column(get_size, verbose_name=_("Size"))
attachments = tables.Column(get_attachment,
verbose_name=_("Attachments"),
empty_value=_("-"))
status = tables.Column("status", filters=(title,))
class Meta:
name = "volumes"
verbose_name = _("Volumes")
table_actions = (CreateVolume, DeleteVolume,)
row_actions = (EditAttachments, DeleteVolume,)
class DetachVolume(tables.BatchAction):
name = "detach"
action_present = _("Detach")
action_past = _("Detached")
data_type_singular = _("Volume")
data_type_plural = _("Volumes")
classes = ('danger',)
def action(self, request, obj_id):
instance_id = self.table.get_object_by_id(obj_id)['serverId']
api.volume_detach(request, instance_id, obj_id)
def get_success_url(self, request):
return reverse('horizon:nova:instances_and_volumes:index')
class AttachmentsTable(tables.DataTable):
instance = tables.Column("serverId", verbose_name=_("Instance"))
device = tables.Column("device")
def sanitize_id(self, obj_id):
return int(obj_id)
def get_object_id(self, obj):
return obj['id']
def get_object_display(self, obj):
vals = {"dev": obj['device'],
"instance": obj['serverId']}
return "Attachment %(dev)s on %(instance)s" % vals
def get_object_by_id(self, obj_id):
for obj in self.data:
print self.get_object_id(obj)
if self.get_object_id(obj) == obj_id:
return obj
raise ValueError('No match found for the id "%s".' % obj_id)
class Meta:
name = "attachments"
table_actions = (DetachVolume,)
row_actions = (DetachVolume,)

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

@ -26,37 +26,16 @@ from django.utils.translation import ugettext as _
from novaclient import exceptions as novaclient_exceptions
from horizon import api
from horizon.dashboards.nova.instances_and_volumes.volumes.forms \
import (CreateForm, DeleteForm, AttachForm, DetachForm)
from horizon import exceptions
from horizon import forms
from horizon import tables
from .forms import CreateForm, AttachForm
from .tables import AttachmentsTable
LOG = logging.getLogger(__name__)
def index(request):
delete_form, handled = DeleteForm.maybe_handle(request)
detach_form, handled = DetachForm.maybe_handle(request)
if handled:
return handled
create_form = CreateForm()
try:
volumes = api.volume_list(request)
except novaclient_exceptions.ClientException, e:
volumes = []
LOG.exception("ClientException in volume index")
messages.error(request, _('Error fetching volumes: %s') % e.message)
return shortcuts.render(request,
'nova/instances_and_volumes/volumes/index.html', {
'volumes': volumes,
'delete_form': delete_form,
'create_form': create_form,
'detach_form': detach_form})
def detail(request, volume_id):
try:
volume = api.volume_get(request, volume_id)
@ -79,27 +58,48 @@ def detail(request, volume_id):
'instance': instance})
def create(request):
create_form, handled = CreateForm.maybe_handle(request)
if handled:
return handled
return shortcuts.render(request,
'nova/instances_and_volumes/volumes/create.html', {
'create_form': create_form})
class CreateView(forms.ModalFormView):
form_class = CreateForm
template_name = 'nova/instances_and_volumes/volumes/create.html'
def attach(request, volume_id):
instances = api.server_list(request)
attach_form, handled = AttachForm.maybe_handle(request,
initial={'volume_id': volume_id,
'instances': instances})
class EditAttachmentsView(tables.DataTableView):
table_class = AttachmentsTable
template_name = 'nova/instances_and_volumes/volumes/attach.html'
if handled:
return handled
def get_data(self):
volume_id = self.kwargs['volume_id']
try:
self.object = api.volume_get(self.request, volume_id)
attachments = [att for att in self.object.attachments if att]
except:
self.object = None
attachments = []
exceptions.handle(self.request,
_('Unable to retrieve volume information.'))
return attachments
return shortcuts.render(request,
'nova/instances_and_volumes/volumes/attach.html', {
'attach_form': attach_form,
'volume_id': volume_id})
def handle_form(self):
instances = api.nova.server_list(self.request)
initial = {'volume_id': self.kwargs["volume_id"],
'instances': instances}
return AttachForm.maybe_handle(self.request, initial=initial)
def get(self, request, *args, **kwargs):
form, handled = self.handle_form()
if handled:
return handled
tables = self.get_tables()
if not self.object:
return shortcuts.redirect("horizon:nova:instances_and_volumes:"
"index")
context = self.get_context_data(**kwargs)
context['form'] = form
context['volume'] = self.object
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
form, handled = self.handle_form()
if handled:
return handled
return super(EditAttachmentsView, self).post(request, *args, **kwargs)

@ -0,0 +1,132 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2011 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
from django import http
from django.contrib import messages
from django.core.urlresolvers import reverse
from mox import IsA, IgnoreArg
from novaclient import exceptions as nova_exceptions
from horizon import api
from horizon import test
INDEX_URL = reverse('horizon:nova:overview:index')
class InstanceViewTests(test.BaseViewTests):
def setUp(self):
super(InstanceViewTests, self).setUp()
self.now = self.override_times()
server = api.Server(None, self.request)
server.id = "1"
server.name = 'serverName'
server.status = "ACTIVE"
volume = api.Volume(self.request)
volume.id = "1"
self.servers = (server,)
self.volumes = (volume,)
def tearDown(self):
super(InstanceViewTests, self).tearDown()
self.reset_times()
def test_usage(self):
TEST_RETURN = 'testReturn'
now = self.override_times()
self.mox.StubOutWithMock(api, 'usage_get')
api.usage_get(IsA(http.HttpRequest), self.TEST_TENANT,
datetime.datetime(now.year, now.month, 1,
now.hour, now.minute, now.second),
now).AndReturn(TEST_RETURN)
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:nova:overview:index'))
self.assertTemplateUsed(res, 'nova/overview/usage.html')
self.assertEqual(res.context['usage'], TEST_RETURN)
def test_usage_csv(self):
TEST_RETURN = 'testReturn'
self.mox.StubOutWithMock(api, 'usage_get')
timestamp = datetime.datetime(self.now.year, self.now.month, 1,
self.now.hour, self.now.minute,
self.now.second)
api.usage_get(IsA(http.HttpRequest),
self.TEST_TENANT,
timestamp,
self.now).AndReturn(TEST_RETURN)
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:nova:overview:index') +
"?format=csv")
self.assertTemplateUsed(res, 'nova/overview/usage.csv')
self.assertEqual(res.context['usage'], TEST_RETURN)
def test_usage_exception(self):
self.mox.StubOutWithMock(api, 'usage_get')
timestamp = datetime.datetime(self.now.year, self.now.month, 1,
self.now.hour, self.now.minute,
self.now.second)
exception = nova_exceptions.ClientException(500)
api.usage_get(IsA(http.HttpRequest),
self.TEST_TENANT,
timestamp,
self.now).AndRaise(exception)
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:nova:overview:index'))
self.assertTemplateUsed(res, 'nova/overview/usage.html')
self.assertEqual(res.context['usage']._apiresource, None)
def test_usage_default_tenant(self):
TEST_RETURN = 'testReturn'
self.mox.StubOutWithMock(api, 'usage_get')
timestamp = datetime.datetime(self.now.year, self.now.month, 1,
self.now.hour, self.now.minute,
self.now.second)
api.usage_get(IsA(http.HttpRequest),
self.TEST_TENANT,
timestamp,
self.now).AndReturn(TEST_RETURN)
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:nova:overview:index'))
self.assertTemplateUsed(res, 'nova/overview/usage.html')
self.assertEqual(res.context['usage'], TEST_RETURN)

@ -21,6 +21,6 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('horizon.dashboards.nova',
url(r'^$', 'instances_and_volumes.instances.views.usage', name='index'),
urlpatterns = patterns('horizon.dashboards.nova.overview.views',
url(r'^$', 'usage', name='index'),
)

@ -0,0 +1,98 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2011 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import division
import datetime
import logging
from django import http
from django import shortcuts
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext as _
import horizon
from horizon import api
from horizon import exceptions
from horizon import forms
from horizon import test
from horizon import views
LOG = logging.getLogger(__name__)
def usage(request, tenant_id=None):
tenant_id = tenant_id or request.user.tenant_id
today = test.today()
date_start = datetime.date(today.year, today.month, 1)
datetime_start = datetime.datetime.combine(date_start, test.time())
datetime_end = test.utcnow()
show_terminated = request.GET.get('show_terminated', False)
try:
usage = api.usage_get(request, tenant_id, datetime_start, datetime_end)
except:
usage = api.nova.Usage(None)
redirect = reverse("horizon:nova:overview:index")
exceptions.handle(request,
_('Unable to retrieve usage information.'))
ram_unit = "MB"
total_ram = getattr(usage, 'total_active_ram_size', 0)
if total_ram >= 1024:
ram_unit = "GB"
total_ram /= 1024
instances = []
terminated = []
if hasattr(usage, 'instances'):
now = datetime.datetime.now()
for i in usage.instances:
i['uptime_at'] = now - datetime.timedelta(seconds=i['uptime'])
if i['ended_at'] and not show_terminated:
terminated.append(i)
else:
instances.append(i)
if request.GET.get('format', 'html') == 'csv':
template = 'nova/overview/usage.csv'
mimetype = "text/csv"
else:
template = 'nova/overview/usage.html'
mimetype = "text/html"
dash_url = horizon.get_dashboard('nova').get_absolute_url()
return shortcuts.render(request, template, {
'usage': usage,
'ram_unit': ram_unit,
'total_ram': total_ram,
'csv_link': '?format=csv',
'show_terminated': show_terminated,
'datetime_start': datetime_start,
'datetime_end': datetime_end,
'instances': instances,
'dash_url': dash_url},
content_type=mimetype)

@ -1,23 +1,21 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
<div id="security_group_rule_modal" class="{% block modal_class %}modal{% if hide %} hide {% else %} static_page{% endif %}{% endblock %}">
<div class="modal-header">
{% if hide %}<a href="#" class="close">&times;</a>{% endif %}
<h3>Edit Security Group Rules</h3>
</div>
<div class="modal-body clearfix">
<div class="right">
{{ table.render }}
</div>
<form id="edit_security_group_rule_form" action="{% url horizon:nova:access_and_security:security_groups:edit_rules security_group.id %}" method="post">
{% csrf_token %}
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="modal-footer">
<input class="btn primary pull-right" type="submit" value="{% trans "Add Rule" %}" />
<a href="{% url horizon:nova:access_and_security:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
</div>
</form>
</div>
{% block form_id %}security_group_rule_form{% endblock %}
{% block form_action %}{% url horizon:nova:access_and_security:security_groups:edit_rules security_group.id %}{% endblock %}
{% block form_class %}{{ block.super }} horizontal split_quarter{% endblock %}
{% block modal_id %}security_group_rule_modal{% endblock %}
{% block modal-header %}{% trans "Edit Security Group Rules" %}{% endblock %}
{% block modal-body %}
<h3>{% trans "Add Rule" %}</h3>
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
{% endblock %}
{% block modal-footer %}
<input class="btn primary pull-right" type="submit" value="{% trans "Add Rule" %}" />
<a href="{% url horizon:nova:access_and_security:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

@ -48,5 +48,5 @@
{% block modal-footer %}
<input class="btn primary pull-right" type="submit" value="{% trans "Launch Instance" %}" />
<a href="{% url horizon:nova:images_and_snapshots:images:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url horizon:nova:images_and_snapshots:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

@ -21,7 +21,7 @@
<div class="alert-message block-message info">
<p><strong>{% trans "Info: " %}</strong>{% trans "There are currently no snapshots. You can create snapshots from running instances." %}</p>
<div class="alert-actions">
<a class="btn small primary" href='{% url horizon:nova:instances_and_volumes:instances:index %}'>{% trans "View Running Instances" %}</a>
<a class="btn small primary" href='{% url horizon:nova:instances_and_volumes:index %}'>{% trans "View Running Instances" %}</a>
</div>
</div>
{% endif %}

@ -7,21 +7,11 @@
{% endblock page_header %}
{% block dash_main %}
{% if instances %}
{% include 'nova/instances_and_volumes/instances/_list.html' %}
{% else %}
{% include 'nova/instances_and_volumes/instances/_no_instances.html' %}
{% endif %}
<div id="instances">
{{ instances_table.render }}
</div>
{% if volumes %}
{% include 'nova/instances_and_volumes/volumes/_list.html' %}
{% else %}
<div class="alert-message block-message info">
<p><strong>{% trans "Info: " %}</strong>{% trans "There are currently no volumes." %}</p>
<div class="alert-actions">
<a id="volume_create_link" class="btn primary small" data-controls-modal="create_volume_modal" data-backdrop="static" href="{% url horizon:nova:instances_and_volumes:volumes:create %}">{% trans "Create New Volume" %}</a>
</div>
</div>
{% endif %}
{% include 'nova/instances_and_volumes/volumes/_create.html' with form=create_form hide=True %}
<div id="volumes">
{{ volumes_table.render }}
</div>
{% endblock %}

@ -0,0 +1,10 @@
{% for ip_group, addresses in instance.addresses.items %}
{% if instance.addresses.items|length > 1 %}
<h4>{{ ip_group }}</h4>
{% endif %}
<ul>
{% for address in addresses %}
<li>{{ address.addr }}</li>
{% endfor %}
</ul>
{% endfor %}

@ -1,83 +0,0 @@
{% load sizeformat %}
{% load i18n %}
<div class="table_title">
<h3>{% trans "My Instances" %}</h3>
<div class="table_actions">
<a class="btn small primary" href='{% url horizon:nova:images_and_snapshots:images:index %}'>{% trans "Launch Instance" %}</a>
<div class="instances table_search">
<form action="#">
<input class="span3" type="text">
</form>
</div>
<a class="inspect" href="#">{% trans "inspect" %}</a>
</div>
</div>
<table id='instances' class="zebra-striped sortable">
<thead>
<tr>
<th></th>
<th>{% trans "Name" %}</th>
<th>{% trans "IP" %}</th>
<th>{% trans "Size" %}</th>
<th>{% trans "State" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for instance in instances %}
<tr class="{% cycle 'odd' 'even' %}">
<td class="select">
<input type="checkbox" name="instance_{{ instance.id }}" value="instance_{{ instance.id }}" id="instance_select_{{ instance.id }}" />
</td>
<td class="name">
<a href="{% url horizon:nova:instances_and_volumes:instances:detail instance.id %}">{{ instance.name }}</a>
</td>
<td>
{% for ip_group, addresses in instance.addresses.items %}
{% if instance.addresses.items|length > 1 %}
<h4>{{ip_group}}</h4>
<ul>
{% for address in addresses %}
<li>{{address.addr}}</li>
{% endfor %}
</ul>
{% else %}
<ul>
{% for address in addresses %}
<li>{{address.addr}}</li>
{% endfor %}
</ul>
{% endif %}
{% endfor %}
</td>
<td>
{{ instance.full_flavor.ram|mbformat }} Ram | {{ instance.full_flavor.vcpus }} VCPU | {{ instance.full_flavor.disk }}GB Disk
</td>
<td>{{ instance.status|lower|capfirst }}</td>
<td id="name_{{ instance.name }}" class="actions">
<a class="more-actions" href="#">More</a>
<ul>
{% if instance.status == 'PAUSED' %}
<li>{% include 'nova/instances_and_volumes/instances/_unpause.html' with form=unpause_form %}</li>
{% endif %}
{% if instance.status == 'SUSPENDED' %}
<li>{% include 'nova/instances_and_volumes/instances/_resume.html' with form=resume_form %}</li>
{% endif %}
{% if instance.status == "ACTIVE" %}
<li><a class="btn small" target='_blank' href='{% url horizon:nova:instances_and_volumes:instances:vnc instance.id %}'>{% trans 'VNC Console' %}</a></li>
<li><a class='btn small' target='_blank' href='{% url horizon:nova:instances_and_volumes:instances:console instance.id %}'>{% trans 'Log' %}</a></li>
<li><a class='btn small' href='{% url horizon:nova:images_and_snapshots:snapshots:create instance.id %}'>{% trans 'Snapshot' %}</a></li>
<li>{% include 'nova/instances_and_volumes/instances/_pause.html' with form=pause_form %}</li>
<li>{% include 'nova/instances_and_volumes/instances/_suspend.html' with form=suspend_form %}</li>
<li>{% include 'nova/instances_and_volumes/instances/_reboot.html' with form=reboot_form %}</li>
{% endif %}
<li><a class='btn small' href='{% url horizon:nova:instances_and_volumes:instances:update instance.id %}'>{% trans 'Edit' %}</a></li>
<li>{% include 'nova/instances_and_volumes/instances/_terminate.html' with form=terminate_form %}</li>
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>

@ -20,5 +20,5 @@
{% block modal-footer %}
<input class="btn primary pull-right" type="submit" value="{% trans "Update Instance" %}" />
<a href="{% url horizon:nova:instances_and_volumes:instances:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url horizon:nova:instances_and_volumes:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

@ -1,57 +0,0 @@
{% extends 'nova/base.html' %}
{% load i18n %}
{% block title %}Instances{% endblock %}
{% block page_header %}
{% url horizon:nova:instances_and_volumes:instances:index as refresh_link %}
{# to make searchable false, just remove it from the include statement #}
{% include "horizon/common/_page_header.html" with title=_("Instances") refresh_link=refresh_link searchable="true" %}
{% endblock page_header %}
{% block dash_main %}
{% if instances %}
{% include 'nova/instances_and_volumes/instances/_list.html' %}
{% else %}
{% include 'nova/instances_and_volumes/instances/_no_instances.html' %}
{% endif %}
{% endblock %}
{% block footer_js %}
<script type="text/javascript" charset="utf-8">
$(function(){
function loadInstances(){
if ($("#ajax_option_box").is(':checked')) {
$('.refresh').addClass("refreshing");
$('#instances').load('{% url horizon:nova:instances_and_volumes:instances:refresh %}', function(){
$('.refresh').removeClass("refreshing");
});
};
}
setInterval(function(){
loadInstances();
}, 15000);
loadOptionsWidget();
$("a.refresh").click(function(e){
e.preventDefault()
loadInstances();
})
function loadOptionsWidget(){
checkbox = document.createElement("input");
cb = $(checkbox);
cb.attr('id', 'ajax_option_box');
cb.attr('class', 'refreshOption');
cb.attr('type', 'checkbox');
checkbox_label = document.createElement("label");
cbl = $(checkbox_label);
cbl.attr('class', 'refreshOption');
cbl.text('auto refresh');
cbl.attr('for', 'ajax_option_box');
$('.right').append(cb);
$('.right').append(cbl);
}
})
</script>
{% endblock footer_js %}

@ -3,32 +3,9 @@
{% block title %}Update Instance{% endblock %}
{% block page_header %}
{# to make searchable false, just remove it from the include statement #}
{% include "horizon/common/_page_header.html" with title=_("Update Instance") %}
{% endblock page_header %}
{% block dash_main %}
{% include 'nova/instances_and_volumes/instances/_update.html' with form=form %}
{% include 'nova/instances_and_volumes/instances/_update.html' %}
{% endblock %}
{% block footer_js %}
<script type="text/javascript" charset="utf-8">
$(function(){
$("#spinner").hide()
function loadInstances(){
$('#spinner').show();
$('#instances').load('{% url horizon:nova:instances_and_volumes:instances:refresh %}', function(){
$("#spinner").hide()
});
}
setInterval(function(){
loadInstances();
}, 15000);
$("a.refresh").click(function(e){
e.preventDefault()
loadInstances();
})
})
</script>
{% endblock footer_js %}

@ -2,24 +2,20 @@
{% 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:instances_and_volumes:volumes:attach volume.id %}{% endblock %}
{% block form_class %}{{ block.super }} horizontal split_half{% endblock %}
{% block modal_id %}attach_volume_modal{% endblock %}
{% block modal-header %}{% trans "Attach Volume" %}{% endblock %}
{% block modal-header %}{% trans "Manage Volume Attachments" %}{% endblock %}
{% block modal-body %}
<div class="left">
<h3>{% trans "Attach To Instance" %}</h3>
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description" %}:</h3>
<p>{% trans "Attach a volume to an instance." %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn primary pull-right" type="submit" value="{% trans "Attach Volume" %}" />
<a href="{% url horizon:nova:instances_and_volumes:volumes:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url horizon:nova:instances_and_volumes:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

@ -21,5 +21,5 @@
{% block modal-footer %}
<input class="btn primary pull-right" type="submit" value="{% trans "Create Volume" %}" />
<a href="{% url horizon:nova:instances_and_volumes:volumes:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url horizon:nova:instances_and_volumes:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

@ -1,70 +0,0 @@
{% load i18n %}
{% load parse_date %}
<div class="table_title">
<h3>{% trans "Block Volumes" %}</h3>
<div class="table_actions">
<a id="volume_create_link" class="btn primary small" data-controls-modal="create_volume_modal" data-backdrop="static" href="{% url horizon:nova:instances_and_volumes:volumes:create %}">{% trans "Create New Volume" %}</a>
<div class="instances table_search">
<form action="#">
<input class="span3" type="text">
</form>
</div>
<a class="inspect" href="#">{% trans "inspect" %}</a>
</div>
</div>
<table id="volumes" class="zebra-striped sortable">
<thead>
<tr>
<th></th>
<th>{% trans "Name" %}</th>
<th>{% trans "Size" %}</th>
<th>{% trans "Instance" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for volume in volumes %}
<tr class="{% cycle 'odd' 'even' %}">
<td></t<td class="select">
<input type="checkbox" name="volume_{{ volume.id }}" value="volume_{{ volume.id }}" id="volume_select_{{ volume.id }}" />
</td>
<td>{{ volume.displayName }}</td>
<td>{{ volume.size }} {% trans "GB" %}</td>
<td>
{% for attachment in volume.attachments %}
{% if attachment %}
<a href="{% url horizon:nova:instances_and_volumes:instances:detail attachment.serverId %}">
{# TODO(jake): Make this the instance name #}
Instance {{ attachment.serverId }}
<small>({{ attachment.device }})</small>
</a>
{% else %}
{% trans "Not Attached" %}
{% endif %}
{% endfor %}
</td>
<td id="name_{{instance.name}}" class="actions">
{% if volume.status == "in-use" or volume.status == "available" %}
<a class="more-actions" href="#">More</a>
<ul>
{% if volume.status == "in-use" %}
{% for attachment in volume.attachments %}
<li class="form">{% include "nova/instances_and_volumes/volumes/_detach.html" with form=detach_form %}</li>
{% endfor %}
{% endif %}
{% if volume.status == "available" %}
<li><a class="btn small" href="{% url horizon:nova:instances_and_volumes:volumes:attach volume.id %}">{% trans "Attach" %}</a></li>
<li class="form">{% include "nova/instances_and_volumes/volumes/_delete.html" with form=delete_form %}</li>
{% endif %}
</ul>
{% else %}
None
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>

@ -1,11 +1,11 @@
{% extends 'nova/base.html' %}
{% load i18n %}
{% block title %}Attach Volume{% endblock %}
{% block title %}Manage Volume Attachments{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Attach a Volume") %}
{% include "horizon/common/_page_header.html" with title=_("Manage Volume Attachments") %}
{% endblock page_header %}
{% block dash_main %}
{% include 'nova/instances_and_volumes/volumes/_attach.html' with form=attach_form %}
{% include 'nova/instances_and_volumes/volumes/_attach.html' %}
{% endblock %}

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

@ -1,25 +0,0 @@
{% extends 'nova/base.html' %}
{% load i18n %}
{% block title %}Volumes{% endblock %}
{% block page_header %}
{% url horizon:nova:instances_and_volumes:volumes:index as refresh_link %}
{# to make searchable false, just remove it from the include statement #}
{% include "horizon/common/_page_header.html" with title=_("Volumes") refresh_link=refresh_link searchable="true" %}
{% endblock page_header %}
{% block dash_main %}
{% if volumes %}
{% include 'nova/instances_and_volumes/volumes/_list.html' %}
{% else %}
<div class="alert-message block-message info">
<p><strong>{% trans "Info: " %}</strong>{% trans "There are currently no volumes." %}</p>
<div class="alert-actions">
<a id="volume_create_link" class="btn primary small" data-controls-modal="create_volume_modal" data-backdrop="static" href="{% url horizon:nova:instances_and_volumes:volumes:create %}">{% trans "Create New Volume" %}</a>
</div>
</div>
{% endif %}
{% include 'nova/instances_and_volumes/volumes/_create.html' with form=create_form hide=True %}
{% endblock %}

@ -21,16 +21,15 @@
from django.conf.urls.defaults import *
from django.conf import settings
from .views import DetailView, AdminIndexView
INSTANCES = r'^(?P<instance_id>[^/]+)/%s$'
urlpatterns = patterns('horizon.dashboards.syspanel.instances.views',
url(r'^usage/(?P<tenant_id>[^/]+)$', 'tenant_usage', name='tenant_usage'),
url(r'^$', 'index', name='index'),
url(r'^refresh$', 'refresh', name='refresh'),
url(INSTANCES % 'detail', 'detail', name='detail'),
# NOTE(termie): currently just using the 'dash' versions
#url(INSTANCES % 'console', 'console', name='instances_console'),
#url(INSTANCES % 'vnc', 'vnc', name='syspanel_instances_vnc'),
url(r'^$', AdminIndexView.as_view(), name='index'),
url(INSTANCES % 'detail', DetailView.as_view(), name='detail'),
url(INSTANCES % 'console', 'console', name='console'),
url(INSTANCES % 'vnc', 'vnc', name='vnc'),
)

@ -29,328 +29,27 @@ from django.shortcuts import render_to_response, redirect
from django.utils.translation import ugettext as _
from horizon import api
from horizon import exceptions
from horizon import forms
from horizon.dashboards.nova.instances_and_volumes.instances.forms import (
TerminateInstance, PauseInstance, UnpauseInstance, SuspendInstance,
ResumeInstance, RebootInstance)
from openstackx.api import exceptions as api_exceptions
from horizon import tables
from horizon.dashboards.nova.instances_and_volumes \
.instances.tables import InstancesTable
from horizon.dashboards.nova.instances_and_volumes \
.instances.views import console, DetailView, vnc
LOG = logging.getLogger(__name__)
class GlobalSummary(object):
node_resources = ['vcpus', 'disk_size', 'ram_size']
unit_mem_size = {'disk_size': ['GB', 'TB'], 'ram_size': ['MB', 'GB']}
node_resource_info = ['', 'active_', 'avail_']
class AdminIndexView(tables.DataTableView):
table_class = InstancesTable
template_name = 'syspanel/instances/index.html'
def __init__(self, request):
self.summary = {}
for rsrc in GlobalSummary.node_resources:
for info in GlobalSummary.node_resource_info:
self.summary['total_' + info + rsrc] = 0
self.request = request
self.service_list = []
self.usage_list = []
def service(self):
def get_data(self):
instances = []
try:
self.service_list = api.service_list(self.request)
except api_exceptions.ApiException, e:
self.service_list = []
LOG.exception('ApiException fetching service list '
'in instance usage')
messages.error(self.request,
_('Unable to get service info: %s') % e.message)
return
for service in self.service_list:
if service.type == 'nova-compute':
self.summary['total_vcpus'] += min(service.stats['max_vcpus'],
service.stats.get('vcpus', 0))
self.summary['total_disk_size'] += min(
service.stats['max_gigabytes'],
service.stats.get('local_gb', 0))
self.summary['total_ram_size'] += min(
service.stats['max_ram'],
service.stats['memory_mb']) if 'max_ram' \
in service.stats \
else service.stats.get('memory_mb', 0)
def usage(self, datetime_start, datetime_end):
try:
self.usage_list = api.usage_list(self.request, datetime_start,
datetime_end)
except api_exceptions.ApiException, e:
self.usage_list = []
LOG.exception('ApiException fetching usage list in instance usage'
' on date range "%s to %s"' % (datetime_start,
datetime_end))
messages.error(self.request,
_('Unable to get usage info: %s') % e.message)
return
for usage in self.usage_list:
# FIXME: api needs a simpler dict interface (with iteration)
# - anthony
# NOTE(mgius): Changed this on the api end. Not too much
# neater, but at least its not going into private member
# data of an external class anymore
# usage = usage._info
for k in usage._attrs:
v = usage.__getattr__(k)
if isinstance(v, (float, int)):
if not k in self.summary:
self.summary[k] = 0
self.summary[k] += v
def human_readable(self, rsrc):
if self.summary['total_' + rsrc] > 1023:
self.summary['unit_' + rsrc] = GlobalSummary.unit_mem_size[rsrc][1]
mult = 1024.0
else:
self.summary['unit_' + rsrc] = GlobalSummary.unit_mem_size[rsrc][0]
mult = 1.0
for kind in GlobalSummary.node_resource_info:
self.summary['total_' + kind + rsrc + '_hr'] = \
self.summary['total_' + kind + rsrc] / mult
def avail(self):
for rsrc in GlobalSummary.node_resources:
self.summary['total_avail_' + rsrc] = \
self.summary['total_' + rsrc] - \
self.summary['total_active_' + rsrc]
def _next_month(date_start):
y = date_start.year + (date_start.month + 1) / 13
m = ((date_start.month + 1) % 13)
if m == 0:
m = 1
return datetime.date(y, m, 1)
def _current_month():
today = datetime.date.today()
return datetime.date(today.year, today.month, 1)
def _get_start_and_end_date(request):
try:
date_start = datetime.date(
int(request.GET['date_year']),
int(request.GET['date_month']),
1)
except:
today = datetime.date.today()
date_start = datetime.date(today.year, today.month, 1)
date_end = _next_month(date_start)
datetime_start = datetime.datetime.combine(date_start, datetime.time())
datetime_end = datetime.datetime.combine(date_end, datetime.time())
if date_end > datetime.date.today():
datetime_end = datetime.datetime.utcnow()
return (date_start, date_end, datetime_start, datetime_end)
def _csv_usage_link(date_start):
return "?date_month=%s&date_year=%s&format=csv" % (date_start.month,
date_start.year)
def usage(request):
(date_start, date_end, datetime_start, datetime_end) = \
_get_start_and_end_date(request)
global_summary = GlobalSummary(request)
if date_start > _current_month():
messages.error(request, _('No data for the selected period'))
date_end = date_start
datetime_end = datetime_start
else:
global_summary.service()
global_summary.usage(datetime_start, datetime_end)
dateform = forms.DateForm()
dateform['date'].field.initial = date_start
global_summary.avail()
global_summary.human_readable('disk_size')
global_summary.human_readable('ram_size')
if request.GET.get('format', 'html') == 'csv':
template_name = 'syspanel/instances/usage.csv'
mimetype = "text/csv"
else:
template_name = 'syspanel/instances/usage.html'
mimetype = "text/html"
return render_to_response(
template_name, {
'dateform': dateform,
'datetime_start': datetime_start,
'datetime_end': datetime_end,
'usage_list': global_summary.usage_list,
'csv_link': _csv_usage_link(date_start),
'global_summary': global_summary.summary,
'external_links': getattr(settings, 'EXTERNAL_MONITORING', []),
}, context_instance=template.RequestContext(request), mimetype=mimetype)
def tenant_usage(request):
tenant_id = request.user.tenant
(date_start, date_end, datetime_start, datetime_end) = \
_get_start_and_end_date(request)
if date_start > _current_month():
messages.error(request, _('No data for the selected period'))
date_end = date_start
datetime_end = datetime_start
dateform = forms.DateForm()
dateform['date'].field.initial = date_start
usage = {}
try:
usage = api.usage_get(request, tenant_id, datetime_start, datetime_end)
except api_exceptions.ApiException, e:
LOG.exception('ApiException getting usage info for tenant "%s"'
' on date range "%s to %s"' % (tenant_id,
datetime_start,
datetime_end))
messages.error(request, _('Unable to get usage info: %s') % e.message)
running_instances = []
terminated_instances = []
if hasattr(usage, 'instances'):
now = datetime.datetime.now()
for i in usage.instances:
# this is just a way to phrase uptime in a way that is compatible
# with the 'timesince' filter. Use of local time intentional
i['uptime_at'] = now - datetime.timedelta(seconds=i['uptime'])
if i['ended_at']:
terminated_instances.append(i)
else:
running_instances.append(i)
if request.GET.get('format', 'html') == 'csv':
template_name = 'syspanel/instances/tenant_usage.csv'
mimetype = "text/csv"
else:
template_name = 'syspanel/instances/tenant_usage.html'
mimetype = "text/html"
return render_to_response(template_name, {
'dateform': dateform,
'datetime_start': datetime_start,
'datetime_end': datetime_end,
'usage': usage,
'csv_link': _csv_usage_link(date_start),
'instances': running_instances + terminated_instances,
'tenant_id': tenant_id,
}, context_instance=template.RequestContext(request), mimetype=mimetype)
def index(request):
for f in (TerminateInstance, PauseInstance, UnpauseInstance,
SuspendInstance, ResumeInstance, RebootInstance):
form, handled = f.maybe_handle(request)
if handled:
return handled
instances = []
try:
instances = api.admin_server_list(request)
except Exception as e:
LOG.exception('Unspecified error in instance index')
if not hasattr(e, 'message'):
e.message = str(e)
messages.error(request,
_('Unable to get instance list: %s') % e.message)
# We don't have any way of showing errors for these, so don't bother
# trying to reuse the forms from above
terminate_form = TerminateInstance()
pause_form = PauseInstance()
unpause_form = UnpauseInstance()
suspend_form = SuspendInstance()
resume_form = ResumeInstance()
reboot_form = RebootInstance()
return render_to_response(
'syspanel/instances/index.html', {
'instances': instances,
'terminate_form': terminate_form,
'pause_form': pause_form,
'unpause_form': unpause_form,
'suspend_form': suspend_form,
'resume_form': resume_form,
'reboot_form': reboot_form,
}, context_instance=template.RequestContext(request))
def refresh(request):
for f in (TerminateInstance, PauseInstance, UnpauseInstance,
SuspendInstance, ResumeInstance, RebootInstance):
form, handled = f.maybe_handle(request)
if handled:
return handled
instances = []
try:
instances = api.admin_server_list(request)
except Exception as e:
if not hasattr(e, 'message'):
e.message = str(e)
messages.error(request,
_('Unable to get instance list: %s') % e.message)
# We don't have any way of showing errors for these, so don't bother
# trying to reuse the forms from above
terminate_form = TerminateInstance()
pause_form = PauseInstance()
unpause_form = UnpauseInstance()
suspend_form = SuspendInstance()
resume_form = ResumeInstance()
reboot_form = RebootInstance()
return render_to_response(
'syspanel/instances/_list.html', {
'instances': instances,
'terminate_form': terminate_form,
'pause_form': pause_form,
'unpause_form': unpause_form,
'suspend_form': suspend_form,
'resume_form': resume_form,
'reboot_form': reboot_form,
}, context_instance=template.RequestContext(request))
def detail(request, instance_id):
try:
instance = api.server_get(request, instance_id)
try:
console = api.console_create(request, instance_id, 'vnc')
vnc_url = "%s&title=%s(%s)" % (console.output,
instance.name,
instance_id)
except api_exceptions.ApiException, e:
LOG.exception('ApiException while fetching instance vnc \
connection')
messages.error(request,
_('Unable to get vnc console for instance %(inst)s: %(message)s') %
{"inst": instance_id, "message": e.message})
return redirect('horizon:syspanel:instances:index', tenant_id)
except api_exceptions.ApiException, e:
LOG.exception('ApiException while fetching instance info')
messages.error(request,
_('Unable to get information for instance %(inst)s: %(message)s') %
{"inst": instance_id, "message": e.message})
return redirect('horizon:syspanel:instances:index', tenant_id)
return render_to_response(
'syspanel/instances/detail.html', {
'instance': instance,
'vnc_url': vnc_url,
}, context_instance=template.RequestContext(request))
instances = api.admin_server_list(self.request)
except:
exceptions.handle(self.request,
_('Unable to retrieve instance list.'))
return instances

@ -21,6 +21,6 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('horizon.dashboards.syspanel',
url(r'^$', 'instances.views.usage', name='index'),
urlpatterns = patterns('horizon.dashboards.syspanel.overview.views',
url(r'^$', 'usage', name='index'),
)

@ -0,0 +1,176 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2011 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import logging
from dateutil.relativedelta import relativedelta
from django import template
from django import http
from django import shortcuts
from django.conf import settings
from django.contrib import messages
from django.utils.translation import ugettext as _
from horizon import api
from horizon import forms
from horizon import exceptions
LOG = logging.getLogger(__name__)
class GlobalSummary(object):
node_resources = ['vcpus', 'disk_size', 'ram_size']
unit_mem_size = {'disk_size': ['GB', 'TB'], 'ram_size': ['MB', 'GB']}
node_resource_info = ['', 'active_', 'avail_']
def __init__(self, request):
self.summary = {}
for rsrc in GlobalSummary.node_resources:
for info in GlobalSummary.node_resource_info:
self.summary['total_' + info + rsrc] = 0
self.request = request
self.service_list = []
self.usage_list = []
def service(self):
try:
self.service_list = api.service_list(self.request)
except:
self.service_list = []
exceptions.handle(self.request,
_('Unable to retrieve service information.'))
for service in self.service_list:
if service.type == 'nova-compute':
self.summary['total_vcpus'] += min(service.stats['max_vcpus'],
service.stats.get('vcpus', 0))
self.summary['total_disk_size'] += min(
service.stats['max_gigabytes'],
service.stats.get('local_gb', 0))
self.summary['total_ram_size'] += min(
service.stats['max_ram'],
service.stats['memory_mb']) if 'max_ram' \
in service.stats \
else service.stats.get('memory_mb', 0)
def usage(self, start, end):
try:
self.usage_list = api.usage_list(self.request, start, end)
except:
self.usage_list = []
exceptions.handle(self.request,
_('Unable to retrieve usage information on date'
'range %(start)s to %(end)s' % {"start": start,
"end": end}))
for usage in self.usage_list:
for key in usage._attrs:
val = getattr(usage, key)
if isinstance(val, (float, int)):
self.summary.setdefault(key, 0)
self.summary[key] += val
def human_readable(self, rsrc):
if self.summary['total_' + rsrc] > 1023:
self.summary['unit_' + rsrc] = GlobalSummary.unit_mem_size[rsrc][1]
mult = 1024.0
else:
self.summary['unit_' + rsrc] = GlobalSummary.unit_mem_size[rsrc][0]
mult = 1.0
for kind in GlobalSummary.node_resource_info:
self.summary['total_' + kind + rsrc + '_hr'] = \
self.summary['total_' + kind + rsrc] / mult
def avail(self):
for rsrc in GlobalSummary.node_resources:
self.summary['total_avail_' + rsrc] = \
self.summary['total_' + rsrc] - \
self.summary['total_active_' + rsrc]
@staticmethod
def next_month(date_start):
return date_start + relativedelta(months=1)
@staticmethod
def current_month():
today = datetime.date.today()
return datetime.date(today.year, today.month, 1)
@staticmethod
def get_start_and_end_date(year, month, day=1):
date_start = datetime.date(year, month, day)
date_end = GlobalSummary.next_month(date_start)
datetime_start = datetime.datetime.combine(date_start, datetime.time())
datetime_end = datetime.datetime.combine(date_end, datetime.time())
if date_end > datetime.date.today():
datetime_end = datetime.datetime.utcnow()
return date_start, date_end, datetime_start, datetime_end
@staticmethod
def csv_link(date_start):
return "?date_month=%s&date_year=%s&format=csv" % (date_start.month,
date_start.year)
def usage(request):
today = datetime.date.today()
dateform = forms.DateForm(request.GET, initial={'year': today.year,
"month": today.month})
if dateform.is_valid():
req_year = int(dateform.cleaned_data['year'])
req_month = int(dateform.cleaned_data['month'])
else:
req_year = today.year
req_month = today.month
date_start, date_end, datetime_start, datetime_end = \
GlobalSummary.get_start_and_end_date(req_year, req_month)
global_summary = GlobalSummary(request)
if date_start > GlobalSummary.current_month():
messages.error(request, _('No data for the selected period'))
date_end = date_start
datetime_end = datetime_start
else:
global_summary.service()
global_summary.usage(datetime_start, datetime_end)
global_summary.avail()
global_summary.human_readable('disk_size')
global_summary.human_readable('ram_size')
if request.GET.get('format', 'html') == 'csv':
template = 'syspanel/tenants/usage.csv'
mimetype = "text/csv"
else:
template = 'syspanel/tenants/usage.html'
mimetype = "text/html"
context = {'dateform': dateform,
'datetime_start': datetime_start,
'datetime_end': datetime_end,
'usage_list': global_summary.usage_list,
'csv_link': GlobalSummary.csv_link(date_start),
'global_summary': global_summary.summary,
'external_links': getattr(settings, 'EXTERNAL_MONITORING', [])}
return shortcuts.render(request, template, context, content_type=mimetype)

@ -53,8 +53,8 @@
<li>{% include 'nova/instances_and_volumes/instances/_pause.html' with form=pause_form %}</li>
<li>{% include 'nova/instances_and_volumes/instances/_suspend.html' with form=suspend_form %}</li>
<li class="form">{% include "syspanel/instances/_reboot.html" with form=reboot_form %}</li>
<li><a class="btn small" target="_blank" href="{% url horizon:nova:instances_and_volumes:instances:console instance.id %}">{% trans "Console Log" %}</a></li>
<li><a class="btn small" target="_blank" href="{% url horizon:nova:instances_and_volumes:instances:vnc instance.id %}">{% trans "VNC Console" %}</a></li>
<li><a class="btn small" target="_blank" href="{% url horizon:syspanel:instances:console instance.id %}">{% trans "Console Log" %}</a></li>
<li><a class="btn small" target="_blank" href="{% url horizon:syspanel:instances:vnc instance.id %}">{% trans "VNC Console" %}</a></li>
{% endif %}
<li class="form">{% include "syspanel/instances/_terminate.html" with form=terminate_form %}</li>
</ul>

@ -1,60 +1,11 @@
{% extends 'syspanel/base.html' %}
{% load i18n %}
{% block title %}Instances{% endblock %}
{% block title %}{% trans "Instances" %}{% endblock %}
{% block page_header %}
{% url horizon:syspanel:instances:index as refresh_link %}
{# to make searchable false, just remove it from the include statement #}
{% include "horizon/common/_page_header.html" with title=_("Instances") refresh_link=refresh_link searchable="true" %}
{% include "horizon/common/_page_header.html" with title=_("Instances") %}
{% endblock page_header %}
{% block syspanel_main %}
{% if instances %}
{% include 'syspanel/instances/_list.html' %}
{% else %}
<div class="alert-message block-message info">
{% url horizon:nova:images_and_snapshots:images:index as dash_image_url%}
<p><strong>{% trans "Info: " %}</strong>{% blocktrans %}There are currently no instances. You can launch an instance from the <a class="btn small" href='{{dash_image_url}}'>Images Page.</a>{% endblocktrans %}</p>
</div>
{% endif %}
{{ table.render }}
{% endblock %}
{% block footer_js %}
<script type="text/javascript" charset="utf-8">
$(function(){
function loadInstances(){
if ($("#ajax_option_box").is(':checked')) {
$('.refresh').addClass("refreshing");
$('#instances').load('{% url horizon:syspanel:instances:refresh %}', function(){
$('.refresh').removeClass("refreshing");
});
};
}
setInterval(function(){
loadInstances();
}, 15000);
loadOptionsWidget();
$("a.refresh").click(function(e){
e.preventDefault()
loadInstances();
})
function loadOptionsWidget(){
checkbox = document.createElement("input");
cb = $(checkbox);
cb.attr('id', 'ajax_option_box');
cb.attr('class', 'refreshOption');
cb.attr('type', 'checkbox');
checkbox_label = document.createElement("label");
cbl = $(checkbox_label);
cbl.attr('class', 'refreshOption');
cbl.text('auto refresh');
cbl.attr('for', 'ajax_option_box');
$('.right').append(cb);
$('.right').append(cbl);
}
})
</script>
{% endblock footer_js %}

@ -1,11 +0,0 @@
Usage Report For Period:,{{datetime_start|date:"b. d Y H:i"}},/,{{datetime_end|date:"b. d Y H:i"}}
Tenant ID:,{{usage.tenant_id}}
Total Active VCPUs:,{{usage.total_active_vcpus}}
CPU-HRs Used:,{{usage.total_cpu_usage}}
Total Active Ram (MB):,{{usage.total_active_ram_size}}
Total Disk Size:,{{usage.total_active_disk_size}}
Total Disk Usage:,{{usage.total_disk_usage}}
ID,Name,UserId,VCPUs,RamMB,DiskGB,Flavor,Usage(Hours),Uptime(Seconds),State
{% for instance in usage.instances %}{{instance.id}},{{instance.name|addslashes}},{{instance.user_id|addslashes}},{{instance.vcpus|addslashes}},{{instance.ram_size|addslashes}},{{instance.disk_size|addslashes}},{{instance.flavor|addslashes}},{{instance.hours}},{{instance.uptime}},{{instance.state|capfirst|addslashes}}
{% endfor %}
Can't render this file because it contains an unexpected character in line 1 and column 48.

@ -1,91 +0,0 @@
{% extends 'syspanel/base.html' %}
{% load i18n parse_date sizeformat %}
{% block title %}Tenant Usage Overview{% endblock %}
{% block page_header %}
{# to make searchable false, just remove it from the include statement #}
{% include "horizon/common/_page_header.html" with title=_("System Panel Overview") %}
{% endblock page_header %}
{% block syspanel_main %}
<form action="" method="get" id="date_form">
<!-- {% csrf_token %} -->
<h3> Select a month to query its usage: </h3>
<div class="form-row">
{{ dateform.date }}
<input class="submit" type="submit"/>
</div>
</form>
<div id="usage">
<div class="usage_block">
<h3>CPU</h3>
<ul>
<li><span class="quantity">{{ usage.total_active_vcpus }}</span><span class="unit">Cores</span> Active</li>
<li><span class="quantity">{{ usage.total_cpu_usage|floatformat }}</span><span class="unit">CPU-HR</span> Used</li>
</ul>
</div>
<div class="usage_block">
<h3>RAM</h3>
<ul>
<li><span class="quantity">{{ usage.total_active_ram_size }}</span><span class="unit">MB</span> Active</li>
</ul>
</div>
<div class="usage_block">
<h3>Disk</h3>
<ul>
<li><span class="quantity">{{ usage.total_active_disk_size }}</span><span class="unit">GB</span> Active</li>
<li><span class="quantity">{{ usage.total_disk_usage|floatformat }}</span><span class="unit">GB-HR</span> Used</li>
</ul>
</div>
</div>
<p id="activity" class="tenant">
<span><strong>{% trans "Active Instances" %}:</strong> {{ usage.total_active_instances }}</span>
<span><strong>{% trans "This month's VCPU-Hours" %}:</strong> {{ usage.total_cpu_usage|floatformat }}</span>
<span><strong>{% trans "This month's GB-Hours" %}:</strong> {{ usage.total_disk_usage|floatformat }}</span>
</p>
{% if usage.instances %}
<div class='table_title wide'>
<a class="csv_download_link" href="{{ csv_link }}">{% trans "Download CSV" %} &raquo;</a>
<h3>{% trans "Tenant Usage" %}: {{ tenant_id }}</h3>
</div>
<table class="zebra-striped">
<tr id='headings'>
<th>{% trans "ID" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "User" %}</th>
<th>{% trans "VCPUs" %}</th>
<th>{% trans "Ram Size" %}</th>
<th>{% trans "Disk Size" %}</th>
<th>{% trans "Flavor" %}</th>
<th>{% trans "Uptime" %}</th>
<th>{% trans "Status" %}</th>
</tr>
<tbody class='main'>
{% for instance in instances %}
{% if instance.ended_at %}
<tr class="terminated">
{% else %}
<tr class="{% cycle 'odd' 'even' %}">
{% endif %}
<td>{{ instance.id }}</td>
<td>{{ instance.name }}</td>
<td>{{ instance.user_id }}</td>
<td>{{ instance.vcpus }}</td>
<td>{{ instance.ram_size|mbformat }}</td>
<td>{{ instance.disk_size }}GB</td>
<td>{{ instance.flavor }}</td>
<td>{{ instance.uptime_at|timesince }}</td>
<td>{{ instance.state|capfirst }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
{% endblock %}

@ -20,10 +20,10 @@
{% endif %}
<form action="" method="get" id="date_form">
<!-- {% csrf_token %} -->
<h3>{% trans "Select a month to query its usage" %}: </h3>
<div class="form-row">
{{ dateform.date }}
{{ dateform.month }}
{{ dateform.year }}
<input class="btn small" type="submit"/>
</div>
</form>
@ -72,7 +72,7 @@
</tr>
{% for usage in usage_list %}
<tr>
<td><a href="{% url horizon:syspanel:instances:tenant_usage usage.tenant_id %}">{{ usage.tenant_id }}</a></td>
<td><a href="{% url horizon:syspanel:tenants:usage usage.tenant_id %}">{{ usage.tenant_id }}</a></td>
<td>{{ usage.total_active_instances }}</td>
<td>{{ usage.total_active_vcpus }}</td>
<td>{{ usage.total_active_disk_size|diskgbformat }}</td>

@ -24,6 +24,12 @@ class ViewMembersLink(tables.LinkAction):
url = "horizon:syspanel:tenants:users"
class UsageLink(tables.LinkAction):
name = "usage"
verbose_name = _("View Usage")
url = "horizon:syspanel:tenants:usage"
class EditLink(tables.LinkAction):
name = "update"
verbose_name = _("Edit")
@ -69,6 +75,6 @@ class TenantsTable(tables.DataTable):
class Meta:
name = "tenants"
verbose_name = _("Tenants")
row_actions = (EditLink, ViewMembersLink, ModifyQuotasLink,
row_actions = (EditLink, UsageLink, ViewMembersLink, ModifyQuotasLink,
DeleteTenantsAction)
table_actions = (TenantFilterAction, CreateLink, DeleteTenantsAction)

@ -30,4 +30,6 @@ urlpatterns = patterns('horizon.dashboards.syspanel.tenants.views',
UpdateView.as_view(), name='update'),
url(r'^(?P<tenant_id>[^/]+)/users/$', 'users', name='users'),
url(r'^(?P<tenant_id>[^/]+)/quotas/$',
QuotasView.as_view(), name='quotas'))
QuotasView.as_view(), name='quotas'),
url(r'^(?P<tenant_id>[^/]+)/usage/$', 'usage', name='usage')
)

@ -18,6 +18,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import logging
from django import shortcuts
@ -34,6 +35,8 @@ from .forms import (AddUser, RemoveUser, CreateTenant, UpdateTenant,
UpdateQuotas)
from .tables import TenantsTable
from horizon.dashboards.syspanel.overview.views import GlobalSummary
LOG = logging.getLogger(__name__)
@ -131,3 +134,62 @@ class QuotasView(forms.ModalFormView):
'instances': quotas.instances,
'injected_files': quotas.injected_files,
'cores': quotas.cores}
def usage(request, tenant_id):
today = datetime.date.today()
dateform = forms.DateForm(request.GET, initial={'year': today.year,
"month": today.month})
if dateform.is_valid():
req_year = int(dateform.cleaned_data['year'])
req_month = int(dateform.cleaned_data['month'])
else:
req_year = today.year
req_month = today.month
date_start, date_end, datetime_start, datetime_end = \
GlobalSummary.get_start_and_end_date(req_year, req_month)
if date_start > GlobalSummary.current_month():
messages.error(request, _('No data for the selected period'))
date_end = date_start
datetime_end = datetime_start
usage = {}
try:
usage = api.usage_get(request, tenant_id, datetime_start, datetime_end)
except api_exceptions.ApiException, e:
LOG.exception('ApiException getting usage info for tenant "%s"'
' on date range "%s to %s"' % (tenant_id,
datetime_start,
datetime_end))
messages.error(request, _('Unable to get usage info: %s') % e.message)
running_instances = []
terminated_instances = []
if hasattr(usage, 'instances'):
now = datetime.datetime.now()
for i in usage.instances:
# this is just a way to phrase uptime in a way that is compatible
# with the 'timesince' filter. Use of local time intentional
i['uptime_at'] = now - datetime.timedelta(seconds=i['uptime'])
if i['ended_at']:
terminated_instances.append(i)
else:
running_instances.append(i)
if request.GET.get('format', 'html') == 'csv':
template = 'syspanel/tenants/usage.csv'
mimetype = "text/csv"
else:
template = 'syspanel/tenants/usage.html'
mimetype = "text/html"
context = {'dateform': dateform,
'datetime_start': datetime_start,
'datetime_end': datetime_end,
'usage': usage,
'csv_link': GlobalSummary.csv_link(date_start),
'instances': running_instances + terminated_instances,
'tenant_id': tenant_id}
return shortcuts.render(request, template, context, content_type=mimetype)

@ -18,8 +18,6 @@
Exceptions raised by the Horizon code and the machinery for handling them.
"""
from __future__ import absolute_import
import logging
import sys
@ -154,6 +152,8 @@ def handle(request, message=None, redirect=None, ignore=False, escalate=False):
LOG.debug("Recoverable error: %s" % exc_value)
messages.error(request, message or exc_value)
wrap = True
if redirect:
raise Http302(redirect)
if not escalate:
return # return to normal code flow

@ -20,5 +20,5 @@ from django.forms import *
from django.forms import widgets
# Convenience imports for public API components.
from .base import SelfHandlingForm, SelectDateWidget, DateForm
from .base import SelfHandlingForm, DateForm
from .views import ModalFormView

@ -18,7 +18,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import datetime
from datetime import date
import logging
import re
@ -35,120 +35,6 @@ from horizon import exceptions
LOG = logging.getLogger(__name__)
RE_DATE = re.compile(r'(\d{4})-(\d\d?)-(\d\d?)$')
class SelectDateWidget(widgets.Widget):
"""
A Widget that splits date input into three <select> boxes.
This also serves as an example of a Widget that has more than one HTML
element and hence implements value_from_datadict.
"""
none_value = (0, '---')
month_field = '%s_month'
day_field = '%s_day'
year_field = '%s_year'
def __init__(self, attrs=None, years=None, required=True,
skip_day_field=False):
# years is an optional list/tuple of years to use in
# the "year" select box.
self.attrs = attrs or {}
self.required = required
self.skip_day_field = skip_day_field
if years:
self.years = years
else:
this_year = datetime.date.today().year
self.years = range(this_year, this_year + 10)
def render(self, name, value, attrs=None, skip_day_field=True):
try:
year_val, month_val, day_val = value.year, value.month, value.day
except AttributeError:
year_val = month_val = day_val = None
if isinstance(value, basestring):
if settings.USE_L10N:
try:
input_format = formats.get_format(
'DATE_INPUT_FORMATS')[0]
# Python 2.4 compatibility:
# v = datetime.datetime.strptime(value,
# input_format)
# would be clearer, but datetime.strptime was added in
# Python 2.5
v = datetime.datetime(*(time.strptime(value,
input_format)[0:6]))
year_val, month_val, day_val = v.year, v.month, v.day
except ValueError:
pass
else:
match = RE_DATE.match(value)
if match:
year_val, month_val, day_val = \
[int(v) for v in match.groups()]
choices = [(i, i) for i in self.years]
year_html = self.create_select(name,
self.year_field, value, year_val, choices)
choices = dates.MONTHS.items()
month_html = self.create_select(name,
self.month_field, value, month_val, choices)
choices = [(i, i) for i in range(1, 32)]
day_html = self.create_select(name,
self.day_field, value, day_val, choices)
format = formats.get_format('DATE_FORMAT')
escaped = False
output = []
for char in format:
if escaped:
escaped = False
elif char == '\\':
escaped = True
elif char in 'Yy':
output.append(year_html)
elif char in 'bFMmNn':
output.append(month_html)
elif char in 'dj' and not self.skip_day_field:
output.append(day_html)
return safestring.mark_safe(u'\n'.join(output))
def id_for_label(self, id_):
return '%s_month' % id_
id_for_label = classmethod(id_for_label)
def value_from_datadict(self, data, files, name):
y = data.get(self.year_field % name)
m = data.get(self.month_field % name)
d = data.get(self.day_field % name)
if y == m == d == "0":
return None
if y and m and d:
if settings.USE_L10N:
input_format = formats.get_format('DATE_INPUT_FORMATS')[0]
try:
date_value = datetime.date(int(y), int(m), int(d))
except ValueError:
pass
else:
date_value = utils.datetime_safe.new_date(date_value)
return date_value.strftime(input_format)
else:
return '%s-%s-%s' % (y, m, d)
return data.get(name, None)
def create_select(self, name, field, value, val, choices):
if 'id' in self.attrs:
id_ = self.attrs['id']
else:
id_ = 'id_%s' % name
if not (self.required and val):
choices.insert(0, self.none_value)
local_attrs = self.build_attrs(id=field % id_)
s = widgets.Select(choices=choices)
select_html = s.render(field % name, val, local_attrs)
return select_html
class SelfHandlingForm(Form):
@ -215,10 +101,12 @@ class SelfHandlingForm(Form):
class DateForm(Form):
"""
A :class:`Form <django:django.forms.Form>` subclass that includes a field
called ``date`` which uses :class:`.SelectDateWidget`.
"""
date = DateField(widget=SelectDateWidget(
years=range(datetime.date.today().year, 2009, -1),
skip_day_field=True))
""" A simple form for selecting a start date. """
month = ChoiceField(choices=dates.MONTHS.items())
year = ChoiceField()
def __init__(self, *args, **kwargs):
super(DateForm, self).__init__(*args, **kwargs)
years = [(year, year) for year in xrange(2009, date.today().year + 1)]
years.reverse()
self.fields['year'].choices = years

@ -155,9 +155,10 @@ class Action(BaseAction):
self.handles_multiple = True
if not has_handler and (not has_single or has_multiple):
cls_name = self.__class__.__name__
raise NotImplementedError('You must define either a "handle" '
'method or a "single" or "multiple"'
' method.')
'method or a "single" or "multiple" '
'method on %s.' % cls_name)
if not has_single:
def single(self, data_table, request, object_id):
@ -313,26 +314,12 @@ class BatchAction(Action):
Optional location to redirect after completion of the delete
action. Defaults to the current page.
.. method:: get_success_url(self, request=None)
Optional method that returns the success url.
.. method:: action(self, request, datum_id)
Required method that accepts the specified object information
and performs the action. Return values are discarded, errors
raised are caught and logged.
.. method:: allowed(self, request, datum)
Optional method that returns a boolean indicating whether the
action is allowed for the given input.
"""
completion_url = None
def _conjugate(self, items=None, past=False):
"""Builds combinations like 'Delete Object' and 'Deleted
"""
Builds combinations like 'Delete Object' and 'Deleted
Objects' based on the number of items and `past` flag.
"""
if past:
@ -355,16 +342,21 @@ class BatchAction(Action):
super(BatchAction, self).__init__()
def action(self, request, datum_id):
""" Override to take action on the specified datum. Return
values are ignored, errors raised are caught and logged.
"""
Required. Accepts a single object id and performs the specific action.
Return values are discarded, errors raised are caught and logged.
"""
raise NotImplementedError('action() must be defined for '
'BatchAction: %s' % self.data_type_singular)
def get_completion_url(self, request=None):
def get_success_url(self, request=None):
"""
Returns the URL to redirect to after a successful action.
"""
if self.completion_url:
return self.completion_url
return request.build_absolute_uri()
return request.get_full_path()
def handle(self, table, request, obj_ids):
tenant_id = request.user.tenant_id
@ -407,7 +399,7 @@ class BatchAction(Action):
self._conjugate(action_success, True),
", ".join(action_success)))
return shortcuts.redirect(self.get_completion_url(request))
return shortcuts.redirect(self.get_success_url(request))
class DeleteAction(BatchAction):

@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import collections
import copy
import logging
from operator import attrgetter
@ -107,6 +108,12 @@ class Column(object):
A string to be used for cells which have no data. Defaults to an
empty string.
.. attribute:: filters
A list of functions (often template filters) to be applied to the
value of the data for this column prior to output. This is effectively
a shortcut for writing a custom ``transform`` function in simple cases.
"""
# Used to retain order when instantiating columns on a table
creation_counter = 0
@ -134,7 +141,7 @@ class Column(object):
def __init__(self, transform, verbose_name=None, sortable=False,
link=None, hidden=False, attrs=None, status=False,
status_choices=None, empty_value=None):
status_choices=None, empty_value=None, filters=None):
self._data_cache = {}
if callable(transform):
@ -154,6 +161,7 @@ class Column(object):
self.hidden = hidden
self.status = status
self.empty_value = empty_value or ''
self.filters = filters or []
if status_choices:
self.status_choices = status_choices
@ -183,17 +191,30 @@ class Column(object):
or the return value of the attr:`~horizon.tables.Column.transform`
method for this column.
"""
if datum in self._data_cache:
return self._data_cache[datum]
if self.table.get_object_id(datum) in self._data_cache:
return self._data_cache[self.table.get_object_id(datum)]
# Callable transformations
if callable(self.transform):
return self.transform(datum)
if not hasattr(datum, self.transform) and settings.DEBUG:
messages.error(self.table._meta.request,
_("The attribute %(attr)s doesn't exist on "
"%(obj)s.") % {'attr': self.transform,
'obj': datum})
self._data_cache[datum] = getattr(datum, self.transform, None)
return self._data_cache[datum]
data = self.transform(datum)
# Basic object lookups
elif hasattr(datum, self.transform):
data = getattr(datum, self.transform, None)
# Dict lookups
elif isinstance(datum, collections.Iterable) and \
self.transform in datum:
data = datum.get(self.transform)
else:
if settings.DEBUG:
messages.error(self.table._meta.request,
_("The attribute %(attr)s doesn't exist on "
"%(obj)s.") % {'attr': self.transform,
'obj': datum})
data = None
for filter_func in self.filters:
data = filter_func(data)
self._data_cache[self.table.get_object_id(datum)] = data
return self._data_cache[self.table.get_object_id(datum)]
def get_classes(self):
""" Returns a flattened string of the column's CSS classes. """
@ -264,10 +285,10 @@ class Row(object):
# Convert value to string to avoid accidental type conversion
data = widget.render('object_ids',
str(table.get_object_id(datum)))
column._data_cache[datum] = data
column._data_cache[self.table.get_object_id(datum)] = data
elif column.auto == "actions":
data = table.render_row_actions(datum)
column._data_cache[datum] = data
column._data_cache[self.table.get_object_id(datum)] = data
else:
data = column.get_data(datum)
cell = Cell(datum, data, column, self)
@ -629,6 +650,19 @@ class DataTable(object):
context = template.RequestContext(self._meta.request, extra_context)
return table_template.render(context)
def get_absolute_url(self):
""" Returns the canonical URL for this table.
This is used for the POST action attribute on the form element
wrapping the table. In many cases it is also useful for redirecting
after a successful action on the table.
For convenience it defaults to the value of
``request.get_full_path()``, e.g. the path at which the table
was requested.
"""
return self._meta.request.get_full_path()
def get_empty_message(self):
""" Returns the message to be displayed when there is no data. """
return _("No items to display.")

@ -1,4 +1,4 @@
<form action="" method="POST">{% csrf_token %}
<form action="{{ table.get_absolute_url }}" method="POST">{% csrf_token %}
<div class='table_header'>
<h3 class='table_title'>{{ table }}</h3>
{{ table.render_table_actions }}

@ -1,9 +1,15 @@
<div id="{% block modal_id %}{% endblock %}" class="{% block modal_class %}modal{% if hide %} hide {% else %} static_page{% endif %}{% endblock %}">
<div class="modal-header">
{% if hide %}<a href="#" class="close">&times;</a>{% endif %}
<h3>{% block modal-header %}{% endblock %}</h3>
</div>
{% if table %}
<div class="modal-body">
{{ table.render }}
</div>
<hr />
{% endif %}
<form id="{% block form_id %}{% endblock %}" class="{% block form_class %}{% endblock %}" action="{% block form_action %}{% endblock %}" method="{% block form-method %}POST{% endblock %}" {% block form_attrs %}{% endblock %}>{% csrf_token %}
<div class="modal-header">
{% if hide %}<a href="#" class="close">&times;</a>{% endif %}
<h3>{% block modal-header %}{% endblock %}</h3>
</div>
<div class="modal-body clearfix">
{% block modal-body %}
<fieldset>

@ -0,0 +1,17 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from .base import APIView

@ -0,0 +1,46 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.views import generic
from horizon import exceptions
class APIView(generic.TemplateView):
""" A quick class-based view for putting API data into a template.
Subclasses must define one method, ``get_data``, and a template name
via the ``template_name`` attribute on the class.
Errors within the ``get_data`` function are automatically caught by
the :func:`horizon.exceptions.handle` error handler if not otherwise
caught.
"""
def get_data(request, context, *args, **kwargs):
"""
This method should handle any necessary API calls, update the
context object, and return the context object at the end.
"""
raise NotImplementedError("You must define a get_data method "
"on %s" % self.__class__.__name__)
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
try:
context = self.get_data(request, context, *args, **kwargs)
except:
exceptions.handle(request)
return self.render_to_response(context)

@ -335,19 +335,19 @@ table form {
margin-right: 25px;
}
#main_content table {
table {
margin-bottom: 50px;
}
#main_content table tr td {
table tr td {
vertical-align: middle;
}
#main_content table tr.empty td {
table tr.empty td {
text-align: center;
}
#main_content table tfoot tr td {
table tfoot tr td {
border-top: 1px solid #DDD;
background-color: #F1F1F1;
font-size: 11px;
@ -582,6 +582,30 @@ table form {
position: absolute;
}
.modal form.horizontal .form-field {
float: left;
}
.modal form.horizontal.split_half .form-field {
width: 334px; /* Fits 2 fields to a row */
}
.modal form.horizontal.split_quarter .form-field {
width: 167px; /* Fits 4 fields to a row */
}
.modal form.horizontal fieldset {
width: 100%;
}
.modal table {
margin-bottom: 30px;
}
.modal-body ~ hr {
margin-bottom: 0;
}
.modal.static_page {
position: relative;
left: 250px;
@ -664,28 +688,6 @@ table form {
margin-bottom: 115px;
}
#edit_security_group_rule_form {
float: right;
width: 163px;
}
#edit_security_group_rule_form fieldset {
width: 163px;
}
#security_group_rule_modal .left .help-inline {
float: left;
}
#security_group_rule_modal .right {
width: 475px;
float: right;
margin-left: 25px;
}
#security_group_rule_modal .right table {
margin-top: 31px;
}
#actions {
width: 90px;
}