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:
parent
29b70fbf92
commit
6c359166a6
horizon/horizon
api
dashboards
nova
access_and_security/security_groups
images_and_snapshots
instances_and_volumes
overview
templates/nova
access_and_security/security_groups
images_and_snapshots
instances_and_volumes
overview
syspanel
instances
overview
templates/syspanel
tenants
forms
tables
templates/horizon/common
views
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 ' \
|
||||
'<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)
|
||||
|
132
horizon/horizon/dashboards/nova/overview/tests.py
Normal file
132
horizon/horizon/dashboards/nova/overview/tests.py
Normal file
@ -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'),
|
||||
)
|
||||
|
98
horizon/horizon/dashboards/nova/overview/views.py
Normal file
98
horizon/horizon/dashboards/nova/overview/views.py
Normal file
@ -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)
|
40
horizon/horizon/dashboards/nova/templates/nova/access_and_security/security_groups/_edit_rules.html
40
horizon/horizon/dashboards/nova/templates/nova/access_and_security/security_groups/_edit_rules.html
@ -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">×</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 %}
|
||||
|
10
horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/instances/_instance_ips.html
Normal file
10
horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/instances/_instance_ips.html
Normal file
@ -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 %}
|
Can't render this file because it contains an unexpected character in line 1 and column 48.
|
@ -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'),
|
||||
)
|
||||
|
176
horizon/horizon/dashboards/syspanel/overview/views.py
Normal file
176
horizon/horizon/dashboards/syspanel/overview/views.py
Normal file
@ -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" %} »</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 %}
|
Can't render this file because it contains an unexpected character in line 1 and column 48.
|
@ -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">×</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">×</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
|
46
horizon/horizon/views/base.py
Normal file
46
horizon/horizon/views/base.py
Normal file
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user