Reworked all the usage implementations into one standard set.

Adds a base Usage data object, datatables, class-based views,
and more consistent templating for all cases.

Bumps environment version to ensure latest novaclient.

Fixes bug 922353.

Change-Id: Ib2042e9393c8deb0e3ec23403da55a6fb8dd39fb
This commit is contained in:
Gabriel Hurley 2012-01-31 16:26:24 -08:00
parent 81d6026750
commit 64b81acc0a
26 changed files with 492 additions and 504 deletions

2
.gitignore vendored
View File

@ -2,7 +2,7 @@
*.swp
.environment_version
.selenium_log
.coverage
.coverage*
.noseids
coverage.xml
pep8.txt

View File

@ -130,24 +130,40 @@ class Usage(APIResourceWrapper):
'total_local_gb_usage', 'total_memory_mb_usage',
'total_vcpus_usage', 'total_hours']
def get_summary(self):
return {'instances': self.total_active_instances,
'memory_mb': self.memory_mb,
'vcpus': getattr(self, "total_vcpus_usage", 0),
'vcpu_hours': self.vcpu_hours,
'local_gb': self.local_gb,
'disk_gb_hours': self.disk_gb_hours}
@property
def total_active_instances(self):
return sum(1 for s in self.server_usages if s['ended_at'] == None)
@property
def total_active_vcpus(self):
return sum(s['vcpus']\
for s in self.server_usages if s['ended_at'] == None)
def vcpus(self):
return sum(s['vcpus'] for s in self.server_usages
if s['ended_at'] == None)
@property
def total_active_local_gb(self):
return sum(s['local_gb']\
for s in self.server_usages if s['ended_at'] == None)
def vcpu_hours(self):
return getattr(self, "total_hours", 0)
@property
def total_active_memory_mb(self):
return sum(s['memory_mb']\
for s in self.server_usages if s['ended_at'] == None)
def local_gb(self):
return sum(s['local_gb'] for s in self.server_usages
if s['ended_at'] == None)
@property
def memory_mb(self):
return sum(s['memory_mb'] for s in self.server_usages
if s['ended_at'] == None)
@property
def disk_gb_hours(self):
return getattr(self, "total_local_gb_usage", 0)
class SecurityGroup(APIResourceWrapper):

View File

@ -24,44 +24,74 @@ from django import http
from django.core.urlresolvers import reverse
from mox import IsA
from novaclient import exceptions as nova_exceptions
from novaclient.v1_1 import usage as nova_usage
from horizon import api
from horizon import test
from horizon import usage
INDEX_URL = reverse('horizon:nova:overview:index')
USAGE_DATA = {
'total_memory_mb_usage': 64246.89777777778,
'total_vcpus_usage': 125.48222222222223,
'total_hours': 125.48222222222223,
'total_local_gb_usage': 0.0,
'tenant_id': u'99e7c0197c3643289d89e9854469a4ae',
'stop': u'2012-01-3123: 30: 46',
'start': u'2012-01-0100: 00: 00',
'server_usages': [
{
u'memory_mb': 512,
u'uptime': 442321,
u'started_at': u'2012-01-2620: 38: 21',
u'ended_at': None,
u'name': u'testing',
u'tenant_id': u'99e7c0197c3643289d89e9854469a4ae',
u'state': u'active',
u'hours': 122.87361111111112,
u'vcpus': 1,
u'flavor': u'm1.tiny',
u'local_gb': 0
},
{
u'memory_mb': 512,
u'uptime': 9367,
u'started_at': u'2012-01-3120: 54: 15',
u'ended_at': None,
u'name': u'instance2',
u'tenant_id': u'99e7c0197c3643289d89e9854469a4ae',
u'state': u'active',
u'hours': 2.608611111111111,
u'vcpus': 1,
u'flavor': u'm1.tiny',
u'local_gb': 0
}
]
}
class InstanceViewTests(test.BaseViewTests):
class UsageViewTests(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,)
super(UsageViewTests, self).setUp()
usage_resource = nova_usage.Usage(nova_usage.UsageManager, USAGE_DATA)
self.usage = api.nova.Usage(usage_resource)
self.usages = (self.usage,)
def tearDown(self):
super(InstanceViewTests, self).tearDown()
super(UsageViewTests, 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)
datetime.datetime(now.year, now.month, now.day, now.hour,
now.minute, now.second)) \
.AndReturn(self.usage)
self.mox.ReplayAll()
@ -69,19 +99,21 @@ class InstanceViewTests(test.BaseViewTests):
self.assertTemplateUsed(res, 'nova/overview/usage.html')
self.assertEqual(res.context['usage'], TEST_RETURN)
self.assertTrue(isinstance(res.context['usage'], usage.TenantUsage))
def test_usage_csv(self):
TEST_RETURN = 'testReturn'
now = self.override_times()
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)
timestamp = datetime.datetime(now.year, now.month, 1,
now.hour, now.minute,
now.second)
api.usage_get(IsA(http.HttpRequest),
self.TEST_TENANT,
timestamp,
self.now).AndReturn(TEST_RETURN)
datetime.datetime(now.year, now.month, now.day, now.hour,
now.minute, now.second)) \
.AndReturn(self.usage)
self.mox.ReplayAll()
@ -90,42 +122,46 @@ class InstanceViewTests(test.BaseViewTests):
self.assertTemplateUsed(res, 'nova/overview/usage.csv')
self.assertEqual(res.context['usage'], TEST_RETURN)
self.assertTrue(isinstance(res.context['usage'], usage.TenantUsage))
def test_usage_exception(self):
self.mox.StubOutWithMock(api, 'usage_get')
now = self.override_times()
timestamp = datetime.datetime(self.now.year, self.now.month, 1,
self.now.hour, self.now.minute,
self.now.second)
self.mox.StubOutWithMock(api, 'usage_get')
timestamp = datetime.datetime(now.year, now.month, 1, now.hour,
now.minute, now.second)
exception = nova_exceptions.ClientException(500)
api.usage_get(IsA(http.HttpRequest),
self.TEST_TENANT,
timestamp,
self.now).AndRaise(exception)
datetime.datetime(now.year, now.month, now.day, now.hour,
now.minute, now.second)) \
.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)
self.assertEqual(res.context['usage'].usage_list, [])
def test_usage_default_tenant(self):
TEST_RETURN = 'testReturn'
now = self.override_times()
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)
timestamp = datetime.datetime(now.year, now.month, 1,
now.hour, now.minute,
now.second)
api.usage_get(IsA(http.HttpRequest),
self.TEST_TENANT,
timestamp,
self.now).AndReturn(TEST_RETURN)
datetime.datetime(now.year, now.month, now.day, now.hour,
now.minute, now.second)) \
.AndReturn(self.usage)
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)
self.assertTrue(isinstance(res.context['usage'], usage.TenantUsage))

View File

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

View File

@ -18,75 +18,14 @@
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import division
import datetime
import logging
from django import shortcuts
from django.utils.translation import ugettext as _
import horizon
from horizon import api
from horizon import exceptions
from horizon import time
from horizon import usage
LOG = logging.getLogger(__name__)
class ProjectOverview(usage.UsageView):
table_class = usage.TenantUsageTable
usage_class = usage.TenantUsage
template_name = 'nova/overview/usage.html'
def usage(request, tenant_id=None):
tenant_id = tenant_id or request.user.tenant_id
today = time.today()
date_start = datetime.date(today.year, today.month, 1)
datetime_start = datetime.datetime.combine(date_start, time.time())
datetime_end = time.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)
exceptions.handle(request,
_('Unable to retrieve usage information.'))
total_ram = 0
ram_unit = "MB"
instances = []
terminated = []
if hasattr(usage, 'server_usages'):
total_ram = usage.total_active_memory_mb
now = datetime.datetime.now()
for i in usage.server_usages:
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 total_ram >= 1024:
ram_unit = "GB"
total_ram /= 1024
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)
def get_data(self):
super(ProjectOverview, self).get_data()
return self.usage.get_instances()

View File

@ -1,11 +1,11 @@
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_vcpus_usage}}
Total Active Ram (MB):,{{usage.total_active_memory_mb}}
Total Disk Size:,{{usage.total_active_local_gb}}
Total Disk Usage:,{{usage.total_local_gb_usage}}
Usage Report For Period:,{{ usage.start|date:"b. d Y" }},/,{{ usage.end|date:"b. d Y" }}
Tenant ID:,{{ usage.tenant_id }}
Total Active VCPUs:,{{ usage.summary.instances }}
CPU-HRs Used:,{{ usage.summary.vcpu_hours }}
Total Active Ram (MB):,{{ usage.summary.memory_mb }}
Total Disk Size:,{{ usage.summary.local_gb }}
Total Disk Usage:,{{ usage.summary.disk_gb_hours }}
ID,Name,VCPUs,RamMB,DiskGB,Usage(Hours),Uptime(Seconds),State
{% for server_usage in usage.server_usages %}{{server_usage.id}},{{server_usage.name|addslashes}},{{server_usage.vcpus|addslashes}},{{server_usage.memory_mb|addslashes}},{{server_usage.local_gb|addslashes}},{{server_usage.hours}},{{server_usage.uptime}},{{server_usage.state|capfirst|addslashes}}
Name,VCPUs,RamMB,DiskGB,Usage(Hours),Uptime(Seconds),State
{% for s in usage.get_instances %}{{ s.name|addslashes }},{{ s.vcpus|addslashes }},{{ s.memory_mb|addslashes }},{{ s.local_gb|addslashes }},{{ s.hours }},{{ s.uptime }},{{ s.state|capfirst|addslashes }}
{% endfor %}

Can't render this file because it contains an unexpected character in line 1 and column 48.

View File

@ -7,72 +7,6 @@
{% endblock page_header %}
{% block dash_main %}
{% if usage.server_usages %}
<div id="usage">
<div class="usage_block first">
<h3>CPU</h3>
<ul>
<li><span class="quantity">{{ usage.total_active_vcpus|default:0 }}</span><span class="unit">Cores</span> Active</li>
<li><span class="quantity">{{ usage.total_vcpus_usage|floatformat|default:0 }}</span><span class="unit">CPU-HR</span> Used</li>
</ul>
</div>
<div class="usage_block">
<h3>RAM</h3>
<ul>
<li><span class="quantity">{{ total_ram|default:0 }}</span><span class="unit">{{ ram_unit }}</span> Active</li>
</ul>
</div>
<div class="usage_block last">
<h3>Disk</h3>
<ul>
<li><span class="quantity">{{ usage.total_active_local_gb|default:0 }}</span><span class="unit">GB</span> Active</li>
<li><span class="quantity">{{ usage.total_local_gb|floatformat|default:0 }}</span><span class="unit">GB-HR</span> Used</li>
</ul>
</div>
</div>
<div class='table_title wide'>
<a class="csv_download_link pull-right" href="{{ csv_link }}">{% trans "Download CSV" %}</a>
{% if show_terminated %}
<a id="toggle_terminated" href="{{dash_url}}">{% trans "Hide Terminated" %}</a>
{% else %}
<a id="toggle_terminated" href="{{dash_url}}?show_terminated=True">{% trans "Show Terminated" %}</a>
{% endif %}
<h3>Server Usage Summary</h3>
</div>
<table class="table table-striped table-bordered">
<tr id='headings'>
<th>{% trans "Name" %}</th>
<th>{% trans "Size" %}</th>
<th>{% trans "Uptime" %}</th>
<th>{% trans "State" %}</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.name }}</td>
<td>{{ instance.memory_mb|mbformat }} Ram | {{ instance.vcpus }} VCPU | {{ instance.local_gb }}GB Disk</td>
<td>{{ instance.uptime_at|timesince }}</td>
<td>{{ instance.state|lower|capfirst }}</td>
</tr>
{% empty %}
<tr>
<td colspan=9>{% trans "No active instances." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
{% include 'nova/instances_and_volumes/instances/_no_instances.html' %}
{% endif %}
{% include "horizon/common/_usage_summary.html" %}
{{ table.render }}
{% endblock %}

View File

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

View File

@ -18,108 +18,17 @@
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import logging
from dateutil.relativedelta import relativedelta
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
from horizon import usage
LOG = logging.getLogger(__name__)
class GlobalOverview(usage.UsageView):
table_class = usage.GlobalUsageTable
usage_class = usage.GlobalUsage
template_name = 'syspanel/overview/usage.html'
class GlobalSummary(object):
def __init__(self, request):
self.summary = {}
self.request = request
self.usage_list = []
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}))
# List of attrs on the Usage object that we would like to summarize
attrs = ['total_local_gb_usage', 'total_memory_mb_usage',
'total_active_memory_mb', 'total_vcpus_usage',
'total_active_instances']
for attr in attrs:
for usage in self.usage_list:
self.summary.setdefault(attr, 0)
self.summary[attr] += getattr(usage, attr)
@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'))
datetime_end = datetime_start
else:
global_summary.usage(datetime_start, datetime_end)
if request.GET.get('format', 'html') == 'csv':
template = 'syspanel/tenants/usage.csv'
mimetype = "text/csv"
else:
template = 'syspanel/tenants/global_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)
def get_context_data(self, **kwargs):
context = super(GlobalOverview, self).get_context_data(**kwargs)
context['monitoring'] = getattr(settings, 'EXTERNAL_MONITORING', [])
return context

View File

@ -0,0 +1,9 @@
Usage Report For Period:,{{ usage.start|date:"b. d Y" }},/,{{ usage.end|date:"b. d Y" }}
Active Instances:,{{ usage.summary.instances }}
CPU-HRs Used:,{{ usage.summary.vcpu_hours }}
Total Active Memory (MB):,{{ usage.summary.memory_mb }}
Total Disk Size:,{{ usage.summary.local_gb }}
Total Disk Usage:,{{ usage.summary.disk_gb_hours }}
Tenant,VCPUs,RamMB,DiskGB,Usage(Hours)
{% for u in usage.usage_list %}{{ u.tenant_id|addslashes }},{{ u.vcpus|addslashes }},{{ u.memory_mb|addslashes }},{{ u.local_gb|addslashes }},{{ u.vcpu_hours}}{% endfor %}
Can't render this file because it contains an unexpected character in line 1 and column 46.

View File

@ -0,0 +1,22 @@
{% extends 'syspanel/base.html' %}
{% load i18n sizeformat %}
{% block title %}{% trans "Usage Overview" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title="Overview: "|add:"<small>This page shows overall cloud usage.</small>" %}
{% endblock page_header %}
{% block main %}
{% if monitoring %}
<div id="monitoring">
<h3>{% trans "Monitoring" %}: </h3>
<ul id="external_links">
{% for link in monitoring %}
<li><a target="_blank" href="{{ link.1 }}">{{ link.0 }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% include "horizon/common/_usage_summary.html" %}
{{ table.render }}
{% endblock %}

View File

@ -1,40 +0,0 @@
{% extends 'syspanel/base.html' %}
{% load i18n sizeformat %}
{% block title %}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 %}
{% if external_links %}
<div id="monitoring">
<h3>{% trans "Monitoring" %}: </h3>
<ul id="external_links">
{% for link in external_links %}
<li><a target="_blank" href="{{ link.1 }}">{{ link.0}}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
<form action="" method="get" id="date_form" class="form-horizontal">
<h3>{% trans "Select a month to query its usage" %}: </h3>
<div class="form-row">
{{ dateform.month }}
{{ dateform.year }}
<input class="btn small" type="submit"/>
</div>
</form>
<p id="activity">
<span><strong>{% trans "Active Instances" %}:</strong> {{ global_summary.total_active_instances|default:'-' }}</span>
<span><strong>{% trans "Active Memory" %}:</strong> {{ global_summary.total_active_memory_mb|mbformat|default:'-' }}</span>
<span><strong>{% trans "This month's VCPU-Hours" %}:</strong> {{ global_summary.total_vcpus_usage|floatformat|default:'-' }}</span>
<span><strong>{% trans "This month's GB-Hours" %}:</strong> {{ global_summary.total_local_gb_usage|floatformat|default:'-' }}</span>
</p>
{% block activity_list %}{% endblock %}
{% endblock %}

View File

@ -1,43 +0,0 @@
{% extends 'syspanel/tenants/base_usage.html' %}
{% load i18n sizeformat %}
{% block title %}Usage Overview{% endblock %}
{% block activity_list %}
{% if usage_list %}
<div id="usage">
<table class="table table-striped table-bordered zebra-striped">
<thead>
<tr>
<th>{% trans "Tenant" %}</th>
<th>{% trans "Instances" %}</th>
<th>{% trans "VCPUs" %}</th>
<th>{% trans "Disk" %}</th>
<th>{% trans "RAM" %}</th>
<th>{% trans "VCPU CPU-Hours" %}</th>
<th>{% trans "Disk GB-Hours" %}</th>
</tr>
</thead>
<tbody>
{% for usage in usage_list %}
<tr>
<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_local_gb|diskgbformat }}</td>
<td>{{ usage.total_active_memory_mb|mbformat }}</td>
<td>{{ usage.total_vcpus_usage|floatformat }}</td>
<td>{{ usage.total_local_gb_usage|floatformat }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="7">
<span>Server Usage Summary</span>
<a class="csv_download_link pull-right" href="{{ csv_link }}">{% trans "Download CSV" %} &raquo;</a>
</td>
</td>
</tfoot>
</table>
</div>
{% endif %}
{% endblock %}

View File

@ -1,10 +1,11 @@
Usage Report For Period:,{{datetime_start|date:"b. d Y H:i"}},/,{{datetime_end|date:"b. d Y H:i"}}
Active Instances:,{{global_summary.total_active_instances|default:'-'}}
Active Memory (MB):,{{global_summary.total_active_memory_mb|default:'-'}}
This month's VCPU-Hours:,{{global_summary.total_vcpus_usage|floatformat|default:'-'}}
This month's GB-Hours:,{{global_summary.total_local_gb_usage|floatformat|default:'-'}}
This month's MemoryMB-Hours:,{{global_summary.total_memory_mb_usage|floatformat|default:'-'}}
Usage Report For Period:,{{ usage.start|date:"b. d Y" }},/,{{ usage.end|date:"b. d Y" }}
Tenant ID:,{{ usage.tenant_id }}
Total Active VCPUs:,{{ usage.summary.instances }}
CPU-HRs Used:,{{ usage.summary.vcpu_hours }}
Total Active Ram (MB):,{{ usage.summary.memory_mb }}
Total Disk Size:,{{ usage.summary.local_gb }}
Total Disk Usage:,{{ usage.summary.disk_gb_hours }}
Tenant,Name,VCPUs,RamMB,DiskGB,Usage(Hours),Uptime(Seconds),State
{% for usage in usage_list %}{% for server_usage in usage.server_usages %}{{server_usage.tenant_id|addslashes}},{{server_usage.name|addslashes}},{{server_usage.vcpus|addslashes}},{{server_usage.memory_mb|addslashes}},{{server_usage.local_gb|addslashes}},{{server_usage.hours}},{{server_usage.uptime}},{{server_usage.state|capfirst|addslashes}}{% endfor %}
Name,VCPUs,RamMB,DiskGB,Usage(Hours),Uptime(Seconds),State
{% for s in usage.get_instances %}{{ s.name|addslashes }},{{ s.vcpus|addslashes }},{{ s.memory_mb|addslashes }},{{ s.local_gb|addslashes }},{{ s.hours }},{{ s.uptime }},{{ s.state|capfirst|addslashes }}
{% endfor %}

Can't render this file because it contains an unexpected character in line 1 and column 48.

View File

@ -1,39 +1,8 @@
{% extends 'syspanel/tenants/base_usage.html' %}
{% extends 'syspanel/base.html' %}
{% load i18n sizeformat %}
{% block title %}Usage Overview{% endblock %}
{% block activity_list %}
{% if instances %}
<div id="usage">
{% block title %}{% trans "Tenant Usage Overview" %}{% endblock %}
<table class="table table-striped table-bordered zebra-striped">
<thead>
<tr id="headings">
<th>{% trans "Instances" %}</th>
<th>{% trans "VCPUs" %}</th>
<th>{% trans "Disk" %}</th>
<th>{% trans "RAM" %}</th>
<th>{% trans "Hours" %}</th>
<th>{% trans "Uptime" %}</th>
</tr>
</thead>
<tbody>
{% for instance in instances %}
<tr>
<td>{{ instance.name }}</td>
<td>{{ instance.vcpus }}</td>
<td>{{ instance.local_gb|diskgbformat }}</td>
<td>{{ instance.memory_mb|mbformat }}</td>
<td>{{ instance.hours|floatformat}}</td>
<td>{{ instance.uptime_at|timesince}}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<td colspan="7">
<span>Tenant Server Usage Summary.</span>
<a class="csv_download_link pull-right" href="{{ csv_link }}">{% trans "Download CSV" %} &raquo;</a>
</td>
</tfoot>
</div>
{% endif %}
{% block syspanel_main %}
{% include "horizon/common/_usage_summary.html" %}
{{ table.render }}
{% endblock %}

View File

@ -21,17 +21,18 @@
from django.conf.urls.defaults import patterns, url
from .views import (IndexView, CreateView, UpdateView, QuotasView, UsersView,
AddUserView)
AddUserView, TenantUsageView)
urlpatterns = patterns('horizon.dashboards.syspanel.tenants.views',
urlpatterns = patterns('',
url(r'^$', IndexView.as_view(), name='index'),
url(r'^create$', CreateView.as_view(), name='create'),
url(r'^(?P<tenant_id>[^/]+)/update/$',
UpdateView.as_view(), name='update'),
url(r'^(?P<tenant_id>[^/]+)/quotas/$',
QuotasView.as_view(), name='quotas'),
url(r'^(?P<tenant_id>[^/]+)/usage/$', 'usage', name='usage'),
url(r'^(?P<tenant_id>[^/]+)/usage/$',
TenantUsageView.as_view(), name='usage'),
url(r'^(?P<tenant_id>[^/]+)/users/$', UsersView.as_view(), name='users'),
url(r'^(?P<tenant_id>[^/]+)/users/(?P<user_id>[^/]+)/add/$',
AddUserView.as_view(), name='add_user')

View File

@ -18,11 +18,9 @@
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import logging
import operator
from django import shortcuts
from django import http
from django.contrib import messages
from django.core.urlresolvers import reverse
@ -33,11 +31,10 @@ from horizon import api
from horizon import exceptions
from horizon import forms
from horizon import tables
from horizon import usage
from .forms import AddUser, CreateTenant, UpdateTenant, UpdateQuotas
from .tables import TenantsTable, TenantUsersTable, AddUsersTable
from horizon.dashboards.syspanel.overview.views import GlobalSummary
LOG = logging.getLogger(__name__)
@ -181,60 +178,11 @@ class QuotasView(forms.ModalFormView):
'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)
class TenantUsageView(usage.UsageView):
table_class = usage.TenantUsageTable
usage_class = usage.TenantUsage
template_name = 'syspanel/tenants/usage.html'
if date_start > GlobalSummary.current_month():
messages.error(request, _('No data for the selected period'))
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, 'server_usages'):
now = datetime.datetime.now()
for i in usage.server_usages:
# 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,
'global_summary': usage,
'usage_list': [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)
def get_data(self):
super(TenantUsageView, self).get_data()
return self.usage.get_instances()

View File

@ -209,7 +209,8 @@ class LinkAction(BaseAction):
.. attribute:: url
A string or a callable which resolves to a url to be used as the link
target. (Required)
target. You must either define the ``url`` attribute or a override
the ``get_link_url`` method on the class.
"""
method = "GET"
bound_url = None
@ -224,9 +225,6 @@ class LinkAction(BaseAction):
if not self.verbose_name:
raise NotImplementedError('A LinkAction object must have a '
'verbose_name attribute.')
if not self.url:
raise NotImplementedError('A LinkAction object must have a '
'url attribute.')
if attrs:
self.attrs.update(attrs)
@ -240,6 +238,10 @@ class LinkAction(BaseAction):
When called for a row action, the current row data object will be
passed as the first parameter.
"""
if not self.url:
raise NotImplementedError('A LinkAction class must have a '
'url attribute or define its own '
'get_link_url method.')
if callable(self.url):
return self.url(datum, **self.kwargs)
try:

View File

@ -0,0 +1,17 @@
{% load i18n sizeformat %}
<form action="" method="get" id="date_form">
<h3>{% trans "Select a month to query its usage" %}: </h3>
<div class="form-row">
{{ form.month }}
{{ form.year }}
<input class="btn small" type="submit"/>
</div>
</form>
<p id="activity">
<span><strong>{% trans "Active Instances" %}:</strong> {{ usage.summary.instances|default:'-' }}</span>
<span><strong>{% trans "Active Memory" %}:</strong> {{ usage.summary.memory_mb|mbformat|default:'-' }}</span>
<span><strong>{% trans "This Month's VCPU-Hours" %}:</strong> {{ usage.summary.vcpu_hours|floatformat|default:'-' }}</span>
<span><strong>{% trans "This Month's GB-Hours" %}:</strong> {{ usage.summary.disk_gb_hours|floatformat|default:'-' }}</span>
</p>

View File

@ -1,11 +1,11 @@
import datetime
def time():
def time(hour=0, minute=0, second=0, microsecond=0):
'''Overrideable version of datetime.datetime.today'''
if time.override_time:
return time.override_time
return datetime.time()
return datetime.time(hour, minute, second, microsecond)
time.override_time = None
@ -14,7 +14,7 @@ def today():
'''Overridable version of datetime.datetime.today'''
if today.override_time:
return today.override_time
return datetime.datetime.today()
return datetime.date.today()
today.override_time = None

View File

@ -0,0 +1,19 @@
# 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 BaseUsage, TenantUsage, GlobalUsage
from .views import UsageView
from .tables import BaseUsageTable, TenantUsageTable, GlobalUsageTable

View File

@ -0,0 +1,138 @@
from __future__ import division
import datetime
import logging
from dateutil.relativedelta import relativedelta
from django.contrib import messages
from django.utils.translation import ugettext as _
from horizon import api
from horizon import exceptions
from horizon import forms
from horizon import time
LOG = logging.getLogger(__name__)
class BaseUsage(object):
show_terminated = False
def __init__(self, request, tenant_id=None):
self.tenant_id = tenant_id or request.user.tenant_id
self.request = request
self.summary = {}
self.usage_list = []
@property
def today(self):
return time.today()
@staticmethod
def get_datetime(date, now=False):
if now:
now = time.utcnow()
current_time = time.time(now.hour, now.minute, now.second)
else:
current_time = time.time()
return datetime.datetime.combine(date, current_time)
@staticmethod
def get_start(year, month, day=1):
return datetime.date(year, month, day)
@staticmethod
def get_end(year, month, day=1):
period = relativedelta(months=1)
date_end = BaseUsage.get_start(year, month, day) + period
if date_end > time.today():
date_end = time.today()
return date_end
def get_instances(self):
instance_list = []
[instance_list.extend(u.server_usages) for u in self.usage_list]
return instance_list
def get_date_range(self):
if not hasattr(self, "start") or not hasattr(self, "end"):
args = (self.today.year, self.today.month)
form = self.get_form()
if form.is_valid():
args = (int(form.cleaned_data['year']),
int(form.cleaned_data['month']))
self.start = self.get_start(*args)
self.end = self.get_end(*args)
return self.start, self.end
def get_form(self):
if not hasattr(self, 'form'):
self.form = forms.DateForm(self.request.GET,
initial={'year': self.today.year,
'month': self.today.month})
return self.form
def get_usage_list(self, start, end):
raise NotImplementedError("You must define a get_usage method.")
def get_summary(self):
raise NotImplementedError("You must define a get_summary method.")
def summarize(self, start, end):
if start <= end <= time.today():
# Convert to datetime.datetime just for API call.
start = BaseUsage.get_datetime(start)
end = BaseUsage.get_datetime(end, now=True)
try:
self.usage_list = self.get_usage_list(start, end)
except:
exceptions.handle(self.request,
_('Unable to retrieve usage information.'))
else:
messages.info(self.request,
_("You are viewing data for the future, "
"which may or may not exist."))
for tenant_usage in self.usage_list:
tenant_summary = tenant_usage.get_summary()
for key, value in tenant_summary.items():
self.summary.setdefault(key, 0)
self.summary[key] += value
def csv_link(self):
return "?date_month=%s&date_year=%s&format=csv" % self.get_date_range()
class GlobalUsage(BaseUsage):
show_terminated = True
def get_usage_list(self, start, end):
return api.usage_list(self.request, start, end)
class TenantUsage(BaseUsage):
attrs = ('memory_mb', 'vcpus', 'uptime',
'hours', 'local_gb')
def get_usage_list(self, start, end):
show_terminated = self.request.GET.get('show_terminated',
self.show_terminated)
instances = []
terminated_instances = []
usage = api.usage_get(self.request, self.tenant_id, start, end)
# Attribute may not exist if there are no instances
if hasattr(usage, 'server_usages'):
now = datetime.datetime.now()
for server_usage in usage.server_usages:
# This is a way to phrase uptime in a way that is compatible
# with the 'timesince' filter. (Use of local time intentional.)
server_uptime = server_usage['uptime']
total_uptime = now - datetime.timedelta(seconds=server_uptime)
server_usage['uptime_at'] = total_uptime
if server_usage['ended_at'] and not show_terminated:
terminated_instances.append(server_usage)
else:
instances.append(server_usage)
usage.server_usages = instances
return (usage,)

View File

@ -0,0 +1,56 @@
from django.utils.translation import ugettext as _
from django.template.defaultfilters import timesince
from horizon import tables
from horizon.templatetags.sizeformat import mbformat
class CSVSummary(tables.LinkAction):
name = "csv_summary"
verbose_name = _("Download CSV Summary")
def get_link_url(self, usage=None):
return self.table.kwargs['usage'].csv_link()
class BaseUsageTable(tables.DataTable):
vcpus = tables.Column('vcpus', verbose_name=_("VCPUs"))
disk = tables.Column('local_gb', verbose_name=_("Disk"))
memory = tables.Column('memory_mb',
verbose_name=_("RAM"),
filters=(mbformat,))
hours = tables.Column('vcpu_hours', verbose_name=_("VCPU Hours"))
class GlobalUsageTable(BaseUsageTable):
tenant = tables.Column('tenant_id', verbose_name=_("Tenant ID"))
disk_hours = tables.Column('disk_gb_hours',
verbose_name=_("Disk GB Hours"))
def get_object_id(self, datum):
return datum.tenant_id
class Meta:
name = "global_usage"
verbose_name = _("Usage Summary")
columns = ("tenant", "vcpus", "disk", "memory",
"hours", "disk_hours")
table_actions = (CSVSummary,)
multi_select = False
class TenantUsageTable(BaseUsageTable):
instance = tables.Column('name')
uptime = tables.Column('uptime_at',
verbose_name=_("Uptime"),
filters=(timesince,))
def get_object_id(self, datum):
return datum['name']
class Meta:
name = "tenant_usage"
verbose_name = _("Usage Summary")
columns = ("instance", "vcpus", "disk", "memory", "uptime")
table_actions = (CSVSummary,)
multi_select = False

View File

@ -0,0 +1,52 @@
import logging
from horizon import tables
from .base import BaseUsage
LOG = logging.getLogger(__name__)
class UsageView(tables.DataTableView):
usage_class = None
show_terminated = True
def __init__(self, *args, **kwargs):
super(UsageView, self).__init__(*args, **kwargs)
if not issubclass(self.usage_class, BaseUsage):
raise AttributeError("You must specify a usage_class attribute "
"which is a subclass of BaseUsage.")
def get_template_names(self):
if self.request.GET.get('format', 'html') == 'csv':
return ".".join((self.template_name.rsplit('.', 1)[0], 'csv'))
return self.template_name
def get_content_type(self):
if self.request.GET.get('format', 'html') == 'csv':
return "text/csv"
return "text/html"
def get_data(self):
tenant_id = self.kwargs.get('tenant_id', self.request.user.tenant_id)
self.usage = self.usage_class(self.request, tenant_id)
self.usage.summarize(*self.usage.get_date_range())
self.kwargs['usage'] = self.usage
return self.usage.usage_list
def get_context_data(self, **kwargs):
context = super(UsageView, self).get_context_data(**kwargs)
context['form'] = self.usage.form
context['usage'] = self.usage
return context
def render_to_response(self, context, **response_kwargs):
resp = self.response_class(request=self.request,
template=self.get_template_names(),
context=context,
content_type=self.get_content_type(),
**response_kwargs)
if self.request.GET.get('format', 'html') == 'csv':
resp['Content-Disposition'] = 'attachment; filename=usage.csv'
resp['Content-Type'] = 'text/csv'
return resp

View File

@ -551,10 +551,10 @@ table form {
min-width: 735px;
padding: 5px 0 5px 0;
border: 1px solid #e6e6e6;
margin-bottom: 25px;
clear: both;
}
#activity.tenant { margin: 0 0 0 0; }
#activity span { margin: 0 0 0 10px; }
#activity strong {
@ -574,10 +574,10 @@ table form {
}
#monitoring h3{
font-size: 13px;
font-size: 14px;
font-weight: normal;
float: left;
margin-top: -8px;
line-height: 18px;
}
#external_links, #external_links li {

View File

@ -6,7 +6,7 @@ set -o errexit
# Increment me any time the environment should be rebuilt.
# This includes dependncy changes, directory renames, etc.
# Simple integer secuence: 1, 2, 3...
environment_version=8
environment_version=10
#--------------------------------------------------------#
function usage {
@ -205,6 +205,9 @@ function sanity_check {
selenium=0
fi
fi
# Remove .pyc files. This is sanity checking because they can linger
# after old files are deleted.
find . -name "*.pyc" -exec rm -rf {} \;
}
function backup_environment {
@ -249,13 +252,9 @@ function install_venv {
if [ $quiet -eq 1 ]; then
export PIP_NO_INPUT=true
fi
INSTALL_FAILED=0
python tools/install_venv.py || INSTALL_FAILED=1
if [ $INSTALL_FAILED -eq 1 ]; then
echo "Error updating environment with pip, trying without src packages..."
rm -rf $venv/src
python tools/install_venv.py
fi
echo "Fetching new src packages..."
rm -rf $venv/src
python tools/install_venv.py
command_wrapper="$root/${with_venv}"
# Make sure it worked and record the environment version
sanity_check