diff --git a/.gitignore b/.gitignore index 45f547d88..3016667c0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ *.swp .environment_version .selenium_log -.coverage +.coverage* .noseids coverage.xml pep8.txt diff --git a/horizon/horizon/api/nova.py b/horizon/horizon/api/nova.py index f1b7d4719..338ec539b 100644 --- a/horizon/horizon/api/nova.py +++ b/horizon/horizon/api/nova.py @@ -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): diff --git a/horizon/horizon/dashboards/nova/overview/tests.py b/horizon/horizon/dashboards/nova/overview/tests.py index 2901c7b86..99c4b7229 100644 --- a/horizon/horizon/dashboards/nova/overview/tests.py +++ b/horizon/horizon/dashboards/nova/overview/tests.py @@ -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)) diff --git a/horizon/horizon/dashboards/nova/overview/urls.py b/horizon/horizon/dashboards/nova/overview/urls.py index f168b7933..ed61cf18e 100644 --- a/horizon/horizon/dashboards/nova/overview/urls.py +++ b/horizon/horizon/dashboards/nova/overview/urls.py @@ -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'), ) diff --git a/horizon/horizon/dashboards/nova/overview/views.py b/horizon/horizon/dashboards/nova/overview/views.py index 53c1376b2..787b8c3d7 100644 --- a/horizon/horizon/dashboards/nova/overview/views.py +++ b/horizon/horizon/dashboards/nova/overview/views.py @@ -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() diff --git a/horizon/horizon/dashboards/nova/templates/nova/overview/usage.csv b/horizon/horizon/dashboards/nova/templates/nova/overview/usage.csv index 450f9e022..3a9aa057d 100644 --- a/horizon/horizon/dashboards/nova/templates/nova/overview/usage.csv +++ b/horizon/horizon/dashboards/nova/templates/nova/overview/usage.csv @@ -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 %} diff --git a/horizon/horizon/dashboards/nova/templates/nova/overview/usage.html b/horizon/horizon/dashboards/nova/templates/nova/overview/usage.html index 2f0fb73e1..b100f9f32 100644 --- a/horizon/horizon/dashboards/nova/templates/nova/overview/usage.html +++ b/horizon/horizon/dashboards/nova/templates/nova/overview/usage.html @@ -7,72 +7,6 @@ {% endblock page_header %} {% block dash_main %} - - {% if usage.server_usages %} -
-
-

CPU

- -
- -
-

RAM

- -
- -
-

Disk

- -
-
- -
- {% trans "Download CSV" %} - - {% if show_terminated %} - {% trans "Hide Terminated" %} - {% else %} - {% trans "Show Terminated" %} - {% endif %} - -

Server Usage Summary

-
- - - - - - - - - - {% for instance in instances %} - {% if instance.ended_at %} - - {% else %} - - {% endif %} - - - - - - {% empty %} - - - - {% endfor %} - -
{% trans "Name" %}{% trans "Size" %}{% trans "Uptime" %}{% trans "State" %}
{{ instance.name }}{{ instance.memory_mb|mbformat }} Ram | {{ instance.vcpus }} VCPU | {{ instance.local_gb }}GB Disk{{ instance.uptime_at|timesince }}{{ instance.state|lower|capfirst }}
{% trans "No active instances." %}
- {% else %} - {% include 'nova/instances_and_volumes/instances/_no_instances.html' %} - {% endif %} + {% include "horizon/common/_usage_summary.html" %} + {{ table.render }} {% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/overview/urls.py b/horizon/horizon/dashboards/syspanel/overview/urls.py index 36f3523f4..382cd69ab 100644 --- a/horizon/horizon/dashboards/syspanel/overview/urls.py +++ b/horizon/horizon/dashboards/syspanel/overview/urls.py @@ -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'), ) diff --git a/horizon/horizon/dashboards/syspanel/overview/views.py b/horizon/horizon/dashboards/syspanel/overview/views.py index d7b513337..f35bbf31d 100644 --- a/horizon/horizon/dashboards/syspanel/overview/views.py +++ b/horizon/horizon/dashboards/syspanel/overview/views.py @@ -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 diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/overview/usage.csv b/horizon/horizon/dashboards/syspanel/templates/syspanel/overview/usage.csv new file mode 100644 index 000000000..c4a069bdb --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/overview/usage.csv @@ -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 %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/overview/usage.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/overview/usage.html new file mode 100644 index 000000000..43718ca5d --- /dev/null +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/overview/usage.html @@ -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:"This page shows overall cloud usage." %} +{% endblock page_header %} + +{% block main %} + {% if monitoring %} +
+

{% trans "Monitoring" %}:

+ +
+ {% endif %} + {% include "horizon/common/_usage_summary.html" %} + {{ table.render }} +{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/base_usage.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/base_usage.html deleted file mode 100644 index 23199b11c..000000000 --- a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/base_usage.html +++ /dev/null @@ -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 %} -
-

{% trans "Monitoring" %}:

- -
-{% endif %} - -
-

{% trans "Select a month to query its usage" %}:

-
- {{ dateform.month }} - {{ dateform.year }} - -
-
- -

- {% trans "Active Instances" %}: {{ global_summary.total_active_instances|default:'-' }} - {% trans "Active Memory" %}: {{ global_summary.total_active_memory_mb|mbformat|default:'-' }} - {% trans "This month's VCPU-Hours" %}: {{ global_summary.total_vcpus_usage|floatformat|default:'-' }} - {% trans "This month's GB-Hours" %}: {{ global_summary.total_local_gb_usage|floatformat|default:'-' }} -

- -{% block activity_list %}{% endblock %} - -{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/global_usage.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/global_usage.html deleted file mode 100644 index 9c7b0dd7f..000000000 --- a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/global_usage.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends 'syspanel/tenants/base_usage.html' %} -{% load i18n sizeformat %} -{% block title %}Usage Overview{% endblock %} -{% block activity_list %} - {% if usage_list %} -
- - - - - - - - - - - - - - {% for usage in usage_list %} - - - - - - - - - - {% endfor %} - - - - - - -
{% trans "Tenant" %}{% trans "Instances" %}{% trans "VCPUs" %}{% trans "Disk" %}{% trans "RAM" %}{% trans "VCPU CPU-Hours" %}{% trans "Disk GB-Hours" %}
{{ usage.tenant_id }}{{ usage.total_active_instances }}{{ usage.total_active_vcpus }}{{ usage.total_active_local_gb|diskgbformat }}{{ usage.total_active_memory_mb|mbformat }}{{ usage.total_vcpus_usage|floatformat }}{{ usage.total_local_gb_usage|floatformat }}
- Server Usage Summary - {% trans "Download CSV" %} » -
-
- {% endif %} -{% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/usage.csv b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/usage.csv index 3ea6f81a4..3a9aa057d 100644 --- a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/usage.csv +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/usage.csv @@ -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 %} diff --git a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/usage.html b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/usage.html index ad95070bb..b7bf16aaf 100644 --- a/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/usage.html +++ b/horizon/horizon/dashboards/syspanel/templates/syspanel/tenants/usage.html @@ -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 %} -
+{% block title %}{% trans "Tenant Usage Overview" %}{% endblock %} - - - - - - - - - - - - - {% for instance in instances %} - - - - - - - - - {% endfor %} - - - - - - {% endif %} +{% block syspanel_main %} + {% include "horizon/common/_usage_summary.html" %} + {{ table.render }} {% endblock %} diff --git a/horizon/horizon/dashboards/syspanel/tenants/urls.py b/horizon/horizon/dashboards/syspanel/tenants/urls.py index 18bf74807..598c7c3b9 100644 --- a/horizon/horizon/dashboards/syspanel/tenants/urls.py +++ b/horizon/horizon/dashboards/syspanel/tenants/urls.py @@ -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[^/]+)/update/$', UpdateView.as_view(), name='update'), url(r'^(?P[^/]+)/quotas/$', QuotasView.as_view(), name='quotas'), - url(r'^(?P[^/]+)/usage/$', 'usage', name='usage'), + url(r'^(?P[^/]+)/usage/$', + TenantUsageView.as_view(), name='usage'), url(r'^(?P[^/]+)/users/$', UsersView.as_view(), name='users'), url(r'^(?P[^/]+)/users/(?P[^/]+)/add/$', AddUserView.as_view(), name='add_user') diff --git a/horizon/horizon/dashboards/syspanel/tenants/views.py b/horizon/horizon/dashboards/syspanel/tenants/views.py index 5fc4ce27e..59fdf42a2 100644 --- a/horizon/horizon/dashboards/syspanel/tenants/views.py +++ b/horizon/horizon/dashboards/syspanel/tenants/views.py @@ -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() diff --git a/horizon/horizon/tables/actions.py b/horizon/horizon/tables/actions.py index 64c08d6d3..f6257e535 100644 --- a/horizon/horizon/tables/actions.py +++ b/horizon/horizon/tables/actions.py @@ -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: diff --git a/horizon/horizon/templates/horizon/common/_usage_summary.html b/horizon/horizon/templates/horizon/common/_usage_summary.html new file mode 100644 index 000000000..7efba15f9 --- /dev/null +++ b/horizon/horizon/templates/horizon/common/_usage_summary.html @@ -0,0 +1,17 @@ +{% load i18n sizeformat %} + +
+

{% trans "Select a month to query its usage" %}:

+
+ {{ form.month }} + {{ form.year }} + +
+ + +

+ {% trans "Active Instances" %}: {{ usage.summary.instances|default:'-' }} + {% trans "Active Memory" %}: {{ usage.summary.memory_mb|mbformat|default:'-' }} + {% trans "This Month's VCPU-Hours" %}: {{ usage.summary.vcpu_hours|floatformat|default:'-' }} + {% trans "This Month's GB-Hours" %}: {{ usage.summary.disk_gb_hours|floatformat|default:'-' }} +

diff --git a/horizon/horizon/time.py b/horizon/horizon/time.py index 7cd08083e..a67b56149 100644 --- a/horizon/horizon/time.py +++ b/horizon/horizon/time.py @@ -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 diff --git a/horizon/horizon/usage/__init__.py b/horizon/horizon/usage/__init__.py new file mode 100644 index 000000000..e19fee2bf --- /dev/null +++ b/horizon/horizon/usage/__init__.py @@ -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 diff --git a/horizon/horizon/usage/base.py b/horizon/horizon/usage/base.py new file mode 100644 index 000000000..c569e3686 --- /dev/null +++ b/horizon/horizon/usage/base.py @@ -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,) diff --git a/horizon/horizon/usage/tables.py b/horizon/horizon/usage/tables.py new file mode 100644 index 000000000..c62761e9e --- /dev/null +++ b/horizon/horizon/usage/tables.py @@ -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 diff --git a/horizon/horizon/usage/views.py b/horizon/horizon/usage/views.py new file mode 100644 index 000000000..cebdb6011 --- /dev/null +++ b/horizon/horizon/usage/views.py @@ -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 diff --git a/openstack-dashboard/dashboard/static/dashboard/css/style.css b/openstack-dashboard/dashboard/static/dashboard/css/style.css index 9cfa654c1..5ea499a58 100644 --- a/openstack-dashboard/dashboard/static/dashboard/css/style.css +++ b/openstack-dashboard/dashboard/static/dashboard/css/style.css @@ -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 { diff --git a/run_tests.sh b/run_tests.sh index f9e4cae8a..a31c7181b 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -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
{% trans "Instances" %}{% trans "VCPUs" %}{% trans "Disk" %}{% trans "RAM" %}{% trans "Hours" %}{% trans "Uptime" %}
{{ instance.name }}{{ instance.vcpus }}{{ instance.local_gb|diskgbformat }}{{ instance.memory_mb|mbformat }}{{ instance.hours|floatformat}}{{ instance.uptime_at|timesince}}
- Tenant Server Usage Summary. - {% trans "Download CSV" %} » -