Improvements in csv export for usage data

Added a csv writer using the 'csv' library to format
properly exported data - escaping, encoding etc.
Added a HttpResponse-based class to handle csv generation
Added translation for the CSV columns and template.
Improved consistency for exported data - now passing project name
instead of project id for csv export. Also added both project name/id
in the header of a project usage export.

Fix bug #1158383

Renamed few occurencies of 'tenant' to project.

Also added a new 'project' in nova_data.py, which required
some refactoring of few tests, that didn't consider the current project
for project-based calls.

Note: I've added a StreamingHttpResponse example,
which is introduced in Django 1.5+ and  being advised in the ticket.
However, my opinion is that at the moment we don't need this - it is
too complicated for the current usage.

Change-Id: Ic00626b273921fa5c6c89704b3a9e08b252aaeb0
This commit is contained in:
Tihomir Trifonov 2013-05-22 11:02:04 +03:00
parent 88f2abf396
commit a4e583cffb
18 changed files with 324 additions and 92 deletions

View File

@ -171,8 +171,11 @@ class InstanceViewTest(test.BaseAdminViewTests):
@test.create_stubs({api.nova: ('flavor_list', 'server_list',),
api.keystone: ('tenant_list',)})
def test_index_options_after_migrate(self):
server = self.servers.first()
server.status = "VERIFY_RESIZE"
servers = self.servers.list()
server1 = servers[0]
server1.status = "VERIFY_RESIZE"
server2 = servers[2]
server2.status = "VERIFY_RESIZE"
api.keystone.tenant_list(IsA(http.HttpRequest)) \
.AndReturn(self.tenants.list())
search_opts = {'marker': None, 'paginate': True}

View File

@ -1,10 +1,7 @@
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|floatformat:2 }}
Total Active RAM (MB):,{{ usage.summary.memory_mb }}
Total Disk Size:,{{ usage.summary.local_gb }}
Total Disk Usage:,{{ usage.summary.disk_gb_hours|floatformat:2 }}
{% load i18n %}{% trans "Usage Report For Period" %}:,{{ usage.start|date:"b. d Y" }},{{ usage.end|date:"b. d Y" }}
{% trans "Active Instances" %}:,{{ usage.summary.instances }}
{% trans "CPU-HRs Used" %}:,{{ usage.summary.vcpu_hours|floatformat:2 }}
{% trans "Total Active RAM (MB)" %}:,{{ usage.summary.memory_mb }}
{% trans "Total Disk Size" %}:,{{ usage.summary.local_gb }}
{% trans "Total Disk Usage" %}:,{{ usage.summary.disk_gb_hours|floatformat:2 }}
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|floatformat:2}}
{% endfor %}

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

View File

@ -38,6 +38,7 @@ INDEX_URL = reverse('horizon:project:overview:index')
class UsageViewTests(test.BaseAdminViewTests):
@test.create_stubs({api.nova: ('usage_list',),
quotas: ('tenant_quota_usages',),
api.keystone: ('tenant_list',)})
@ -75,24 +76,26 @@ class UsageViewTests(test.BaseAdminViewTests):
api.keystone: ('tenant_list',)})
def test_usage_csv(self):
now = timezone.now()
usage_obj = api.nova.NovaUsage(self.usages.first())
usage_obj = [api.nova.NovaUsage(u) for u in self.usages.list()]
quota_data = self.quota_usages.first()
api.keystone.tenant_list(IsA(http.HttpRequest)) \
.AndReturn(self.tenants.list())
api.nova.usage_list(IsA(http.HttpRequest),
datetime.datetime(now.year, now.month, 1, 0, 0, 0),
Func(usage.almost_now)) \
.AndReturn([usage_obj, usage_obj])
.AndReturn(usage_obj)
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data)
self.mox.ReplayAll()
csv_url = reverse('horizon:admin:overview:index') + "?format=csv"
res = self.client.get(csv_url)
self.assertTemplateUsed(res, 'admin/overview/usage.csv')
self.assertTrue(isinstance(res.context['usage'], usage.GlobalUsage))
hdr = 'Tenant,VCPUs,RamMB,DiskGB,Usage(Hours)'
row = '%s,%s,%s,%s,%.2f' % (usage_obj.tenant_id,
usage_obj.vcpus,
usage_obj.memory_mb,
usage_obj.disk_gb_hours,
usage_obj.vcpu_hours)
self.assertContains(res, '%s\n%s\n%s\n' % (hdr, row, row))
hdr = 'Project Name,VCPUs,Ram (MB),Disk (GB),Usage (Hours)'
self.assertContains(res, '%s\r\n' % (hdr))
for obj in usage_obj:
row = u'{0},{1},{2},{3},{4:.2f}\r\n'.format(obj.project_name,
obj.vcpus,
obj.memory_mb,
obj.disk_gb_hours,
obj.vcpu_hours)
self.assertContains(res, row)

View File

@ -18,19 +18,38 @@
# License for the specific language governing permissions and limitations
# under the License.
from django import VERSION
from django.conf import settings
from django.template.defaultfilters import floatformat
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from openstack_dashboard import api
from openstack_dashboard import usage
from openstack_dashboard.usage.base import BaseCsvResponse
class GlobalUsageCsvRenderer(BaseCsvResponse):
columns = [_("Project Name"), _("VCPUs"), _("Ram (MB)"),
_("Disk (GB)"), _("Usage (Hours)")]
def get_row_data(self):
for u in self.context['usage'].usage_list:
yield (u.project_name or u.tenant_id,
u.vcpus,
u.memory_mb,
u.local_gb,
floatformat(u.vcpu_hours, 2))
class GlobalOverview(usage.UsageView):
table_class = usage.GlobalUsageTable
usage_class = usage.GlobalUsage
template_name = 'admin/overview/usage.html'
csv_response_class = GlobalUsageCsvRenderer
def get_context_data(self, **kwargs):
context = super(GlobalOverview, self).get_context_data(**kwargs)
@ -39,17 +58,17 @@ class GlobalOverview(usage.UsageView):
def get_data(self):
data = super(GlobalOverview, self).get_data()
# Pre-fill tenant names
# Pre-fill project names
try:
tenants = api.keystone.tenant_list(self.request)
projects = api.keystone.tenant_list(self.request)
except:
tenants = []
projects = []
exceptions.handle(self.request,
_('Unable to retrieve project list.'))
for instance in data:
tenant = filter(lambda t: t.id == instance.tenant_id, tenants)
if tenant:
instance.tenant_name = getattr(tenant[0], "name", None)
project = filter(lambda t: t.id == instance.tenant_id, projects)
if project:
instance.project_name = getattr(project[0], "name", None)
else:
instance.tenant_name = None
instance.project_name = None
return data

View File

@ -20,7 +20,7 @@
from django.conf.urls.defaults import patterns, url
from .views import (IndexView, TenantUsageView,
from .views import (IndexView, ProjectUsageView,
CreateProjectView, UpdateProjectView,
CreateUserView)
@ -31,7 +31,7 @@ urlpatterns = patterns('',
url(r'^(?P<tenant_id>[^/]+)/update/$',
UpdateProjectView.as_view(), name='update'),
url(r'^(?P<tenant_id>[^/]+)/usage/$',
TenantUsageView.as_view(), name='usage'),
ProjectUsageView.as_view(), name='usage'),
url(r'^(?P<tenant_id>[^/]+)/create_user/$',
CreateUserView.as_view(), name='create_user'),
)

View File

@ -116,13 +116,13 @@ class UsersView(tables.MultiTableView):
return context
class TenantUsageView(usage.UsageView):
table_class = usage.TenantUsageTable
usage_class = usage.TenantUsage
class ProjectUsageView(usage.UsageView):
table_class = usage.ProjectUsageTable
usage_class = usage.ProjectUsage
template_name = 'admin/projects/usage.html'
def get_data(self):
super(TenantUsageView, self).get_data()
super(ProjectUsageView, self).get_data()
return self.usage.get_instances()

View File

@ -1,11 +1,9 @@
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|floatformat:2 }}
Total Active Ram (MB):,{{ usage.summary.memory_mb }}
Total Disk Size:,{{ usage.summary.local_gb }}
Total Disk Usage:,{{ usage.summary.disk_gb_hours|floatformat:2 }}
{% load i18n %}{% trans "Usage Report For Period" %}:,{{ usage.start|date:"b. d Y" }},{{ usage.end|date:"b. d Y" }}
{% trans "Project ID" %}:,{{ usage.project_id }}
{% trans "Project Name" %}:,{{ usage.project_name }}
{% trans "Total Active VCPUs" %}:,{{ usage.summary.instances }}
{% trans "CPU-HRs Used" %}:,{{ usage.summary.vcpu_hours|floatformat:2 }}
{% trans "Total Active Ram (MB)" %}:,{{ usage.summary.memory_mb }}
{% trans "Total Disk Size" %}:,{{ usage.summary.local_gb }}
{% trans "Total Disk Usage" %}:,{{ usage.summary.disk_gb_hours|floatformat:2 }}
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|floatformat:2 }},{{ s.uptime }},{{ s.state|capfirst|addslashes }}
{% endfor %}

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

View File

@ -36,22 +36,27 @@ INDEX_URL = reverse('horizon:project:overview:index')
class UsageViewTests(test.TestCase):
@test.create_stubs({api.nova: ('usage_get',),
quotas: ('tenant_quota_usages',),
api.keystone: ('tenant_get',)})
def test_usage(self):
now = timezone.now()
usage_obj = api.nova.NovaUsage(self.usages.first())
quota_data = self.quota_usages.first()
self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(quotas, 'tenant_quota_usages')
project = self.tenants.first()
api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id,
datetime.datetime(now.year, now.month, 1, 0, 0, 0),
Func(usage.almost_now)) \
.AndReturn(usage_obj)
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data)
api.keystone.tenant_get(IsA(http.HttpRequest),
project.id).AndReturn(project)
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:project:overview:index'))
self.assertTemplateUsed(res, 'project/overview/usage.html')
self.assertTrue(isinstance(res.context['usage'], usage.TenantUsage))
self.assertTrue(isinstance(res.context['usage'], usage.ProjectUsage))
self.assertContains(res, 'form-horizontal')
def test_unauthorized(self):
@ -73,12 +78,14 @@ class UsageViewTests(test.TestCase):
self.assertMessageCount(res, error=1)
self.assertContains(res, 'Unauthorized:')
@test.create_stubs({api.nova: ('usage_get',),
quotas: ('tenant_quota_usages',),
api.keystone: ('tenant_get',)})
def test_usage_csv(self):
now = timezone.now()
usage_obj = api.nova.NovaUsage(self.usages.first())
quota_data = self.quota_usages.first()
self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(quotas, 'tenant_quota_usages')
project = self.tenants.first()
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0)
api.nova.usage_get(IsA(http.HttpRequest),
self.tenant.id,
@ -86,18 +93,20 @@ class UsageViewTests(test.TestCase):
Func(usage.almost_now)) \
.AndReturn(usage_obj)
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data)
api.keystone.tenant_get(IsA(http.HttpRequest),
project.id).AndReturn(project)
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:project:overview:index') +
"?format=csv")
self.assertTemplateUsed(res, 'project/overview/usage.csv')
self.assertTrue(isinstance(res.context['usage'], usage.TenantUsage))
self.assertTrue(isinstance(res.context['usage'], usage.ProjectUsage))
@test.create_stubs({api.nova: ('usage_get',),
quotas: ('tenant_quota_usages',)})
def test_usage_exception_usage(self):
now = timezone.now()
quota_data = self.quota_usages.first()
self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(quotas, 'tenant_quota_usages')
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0)
api.nova.usage_get(IsA(http.HttpRequest),
self.tenant.id,
@ -111,11 +120,13 @@ class UsageViewTests(test.TestCase):
self.assertTemplateUsed(res, 'project/overview/usage.html')
self.assertEqual(res.context['usage'].usage_list, [])
@test.create_stubs({api.nova: ('usage_get',),
quotas: ('tenant_quota_usages',),
api.keystone: ('tenant_get',)})
def test_usage_exception_quota(self):
now = timezone.now()
usage_obj = api.nova.NovaUsage(self.usages.first())
self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(quotas, 'tenant_quota_usages')
project = self.tenants.first()
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0)
api.nova.usage_get(IsA(http.HttpRequest),
self.tenant.id,
@ -124,18 +135,22 @@ class UsageViewTests(test.TestCase):
.AndReturn(usage_obj)
quotas.tenant_quota_usages(IsA(http.HttpRequest))\
.AndRaise(self.exceptions.nova)
api.keystone.tenant_get(IsA(http.HttpRequest),
project.id).AndReturn(project)
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:project:overview:index'))
self.assertTemplateUsed(res, 'project/overview/usage.html')
self.assertEqual(res.context['usage'].quotas, {})
@test.create_stubs({api.nova: ('usage_get',),
quotas: ('tenant_quota_usages',),
api.keystone: ('tenant_get',)})
def test_usage_default_tenant(self):
now = timezone.now()
usage_obj = api.nova.NovaUsage(self.usages.first())
quota_data = self.quota_usages.first()
self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(quotas, 'tenant_quota_usages')
project = self.tenants.first()
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0)
api.nova.usage_get(IsA(http.HttpRequest),
self.tenant.id,
@ -143,8 +158,10 @@ class UsageViewTests(test.TestCase):
Func(usage.almost_now)) \
.AndReturn(usage_obj)
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data)
api.keystone.tenant_get(IsA(http.HttpRequest),
project.id).AndReturn(project)
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:project:overview:index'))
self.assertTemplateUsed(res, 'project/overview/usage.html')
self.assertTrue(isinstance(res.context['usage'], usage.TenantUsage))
self.assertTrue(isinstance(res.context['usage'], usage.ProjectUsage))

View File

@ -18,15 +18,38 @@
# License for the specific language governing permissions and limitations
# under the License.
from django import VERSION
from django.template.defaultfilters import floatformat, capfirst
from django.utils.translation import ugettext as _
from django.views.generic import TemplateView
from openstack_dashboard import usage
from openstack_dashboard.usage.base import BaseCsvResponse
class ProjectUsageCsvRenderer(BaseCsvResponse):
columns = [_("Instance Name"), _("VCPUs"), _("Ram (MB)"),
_("Disk (GB)"), _("Usage (Hours)"),
_("Uptime(Seconds)"), _("State")]
def get_row_data(self):
for inst in self.context['usage'].get_instances():
yield (inst['name'],
inst['vcpus'],
inst['memory_mb'],
inst['local_gb'],
floatformat(inst['hours'], 2),
inst['uptime'],
capfirst(inst['state']))
class ProjectOverview(usage.UsageView):
table_class = usage.TenantUsageTable
usage_class = usage.TenantUsage
table_class = usage.ProjectUsageTable
usage_class = usage.ProjectUsage
template_name = 'project/overview/usage.html'
csv_response_class = ProjectUsageCsvRenderer
def get_data(self):
super(ProjectOverview, self).get_data()

View File

@ -609,7 +609,8 @@ class VolumeViewTests(test.TestCase):
@test.create_stubs({cinder: ('volume_get',), api.nova: ('server_list',)})
def test_edit_attachments(self):
volume = self.volumes.first()
servers = self.servers.list()
servers = [s for s in self.servers.list()
if s.tenant_id == self.request.user.tenant_id]
cinder.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume)
api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False])
@ -632,7 +633,8 @@ class VolumeViewTests(test.TestCase):
settings.OPENSTACK_HYPERVISOR_FEATURES['can_set_mount_point'] = False
volume = self.volumes.first()
servers = self.servers.list()
servers = [s for s in self.servers.list()
if s.tenant_id == self.request.user.tenant_id]
cinder.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume)
api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False])
@ -649,13 +651,15 @@ class VolumeViewTests(test.TestCase):
@test.create_stubs({cinder: ('volume_get',),
api.nova: ('server_get', 'server_list',)})
def test_edit_attachments_attached_volume(self):
server = self.servers.first()
servers = [s for s in self.servers.list()
if s.tenant_id == self.request.user.tenant_id]
server = servers[0]
volume = self.volumes.list()[0]
cinder.volume_get(IsA(http.HttpRequest), volume.id) \
.AndReturn(volume)
api.nova.server_list(IsA(http.HttpRequest)) \
.AndReturn([self.servers.list(), False])
.AndReturn([servers, False])
self.mox.ReplayAll()

View File

@ -33,6 +33,7 @@ from openstack_dashboard.test import helpers as test
class ServerWrapperTests(test.TestCase):
def test_get_base_attribute(self):
server = api.nova.Server(self.servers.first(), self.request)
self.assertEqual(server.id, self.servers.first().id)
@ -41,7 +42,7 @@ class ServerWrapperTests(test.TestCase):
image = self.images.first()
self.mox.StubOutWithMock(api.glance, 'image_get')
api.glance.image_get(IsA(http.HttpRequest),
image.id).AndReturn(image)
image.id).AndReturn(image)
self.mox.ReplayAll()
server = api.nova.Server(self.servers.first(), self.request)
@ -49,6 +50,7 @@ class ServerWrapperTests(test.TestCase):
class ComputeApiTests(test.APITestCase):
def test_server_reboot(self):
server = self.servers.first()
HARDNESS = servers.REBOOT_HARD
@ -99,7 +101,7 @@ class ComputeApiTests(test.APITestCase):
novaclient = self.stub_novaclient()
novaclient.servers = self.mox.CreateMockAnything()
novaclient.servers.get_spice_console(server.id,
console_type).AndReturn(console)
console_type).AndReturn(console)
self.mox.ReplayAll()
ret_val = api.nova.server_spice_console(self.request,
@ -148,7 +150,8 @@ class ComputeApiTests(test.APITestCase):
novaclient.servers.list(True,
{'all_tenants': True,
'marker': None,
'limit': page_size + 1}).AndReturn(servers)
'limit': page_size + 1}). \
AndReturn(servers[:page_size + 1])
self.mox.ReplayAll()
ret_val, has_more = api.nova.server_list(self.request,

View File

@ -139,9 +139,14 @@ def data(TEST):
'name': 'disabled_tenant',
'description': "a disabled test tenant.",
'enabled': False}
tenant_dict_unicode = {'id': "3",
'name': u'\u4e91\u89c4\u5219',
'description': "an unicode-named tenant.",
'enabled': True}
tenant = tenants.Tenant(tenants.TenantManager, tenant_dict)
disabled_tenant = tenants.Tenant(tenants.TenantManager, tenant_dict_2)
TEST.tenants.add(tenant, disabled_tenant)
tenant_unicode = tenants.Tenant(tenants.TenantManager, tenant_dict_unicode)
TEST.tenants.add(tenant, disabled_tenant, tenant_unicode)
TEST.tenant = tenant # Your "current" tenant
tomorrow = datetime_safe.datetime.now() + timedelta(days=1)

View File

@ -361,6 +361,8 @@ def data(TEST):
TEST.limits = limits
# Servers
tenant3 = TEST.tenants.list()[2]
vals = {"host": "http://nova.example.com:8774",
"name": "server_1",
"status": "ACTIVE",
@ -377,7 +379,13 @@ def data(TEST):
"server_id": "2"})
server_2 = servers.Server(servers.ServerManager(None),
json.loads(SERVER_DATA % vals)['server'])
TEST.servers.add(server_1, server_2)
vals.update({"name": u'\u4e91\u89c4\u5219',
"status": "ACTIVE",
"tenant_id": tenant3.id,
"server_id": "3"})
server_3 = servers.Server(servers.ServerManager(None),
json.loads(SERVER_DATA % vals)['server'])
TEST.servers.add(server_1, server_2, server_3)
# VNC Console Data
console = {u'console': {u'url': u'http://example.com:6080/vnc_auto.html',
@ -434,6 +442,17 @@ def data(TEST):
json.loads(USAGE_DATA % usage_vals))
TEST.usages.add(usage_obj)
# Usage
usage_2_vals = {"tenant_id": tenant3.id,
"instance_name": server_3.name,
"flavor_name": flavor_1.name,
"flavor_vcpus": flavor_1.vcpus,
"flavor_disk": flavor_1.disk,
"flavor_ram": flavor_1.ram}
usage_obj_2 = usage.Usage(usage.UsageManager(None),
json.loads(USAGE_DATA % usage_2_vals))
TEST.usages.add(usage_obj_2)
volume_snapshot = vol_snaps.Snapshot(vol_snaps.SnapshotManager(None),
{'id': '40f3fabf-3613-4f5e-90e5-6c9a08333fc3',
'display_name': 'test snapshot',

View File

@ -56,6 +56,8 @@ class QuotaTests(test.APITestCase):
quotas: ('is_service_enabled',),
cinder: ('volume_list', 'tenant_quota_get',)})
def test_tenant_quota_usages(self):
servers = [s for s in self.servers.list()
if s.tenant_id == self.request.user.tenant_id]
quotas.is_service_enabled(IsA(http.HttpRequest),
'volume').AndReturn(True)
api.nova.flavor_list(IsA(http.HttpRequest)) \
@ -65,7 +67,7 @@ class QuotaTests(test.APITestCase):
api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \
.AndReturn(self.floating_ips.list())
api.nova.server_list(IsA(http.HttpRequest)) \
.AndReturn([self.servers.list(), False])
.AndReturn([servers, False])
cinder.volume_list(IsA(http.HttpRequest)) \
.AndReturn(self.volumes.list())
cinder.tenant_quota_get(IsA(http.HttpRequest), '1') \
@ -85,6 +87,8 @@ class QuotaTests(test.APITestCase):
api.network: ('tenant_floating_ip_list',),
quotas: ('is_service_enabled',)})
def test_tenant_quota_usages_without_volume(self):
servers = [s for s in self.servers.list()
if s.tenant_id == self.request.user.tenant_id]
quotas.is_service_enabled(IsA(http.HttpRequest),
'volume').AndReturn(False)
api.nova.flavor_list(IsA(http.HttpRequest)) \
@ -94,7 +98,7 @@ class QuotaTests(test.APITestCase):
api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \
.AndReturn(self.floating_ips.list())
api.nova.server_list(IsA(http.HttpRequest)) \
.AndReturn([self.servers.list(), False])
.AndReturn([servers, False])
self.mox.ReplayAll()
@ -143,6 +147,8 @@ class QuotaTests(test.APITestCase):
def test_tenant_quota_usages_unlimited_quota(self):
inf_quota = self.quotas.first()
inf_quota['ram'] = -1
servers = [s for s in self.servers.list()
if s.tenant_id == self.request.user.tenant_id]
quotas.is_service_enabled(IsA(http.HttpRequest),
'volume').AndReturn(True)
@ -153,7 +159,7 @@ class QuotaTests(test.APITestCase):
api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \
.AndReturn(self.floating_ips.list())
api.nova.server_list(IsA(http.HttpRequest)) \
.AndReturn([self.servers.list(), False])
.AndReturn([servers, False])
cinder.volume_list(IsA(http.HttpRequest)) \
.AndReturn(self.volumes.list())
cinder.tenant_quota_get(IsA(http.HttpRequest), '1') \

View File

@ -14,6 +14,6 @@
# License for the specific language governing permissions and limitations
# under the License.
from .base import BaseUsage, TenantUsage, GlobalUsage, almost_now
from .base import BaseUsage, ProjectUsage, GlobalUsage, almost_now
from .views import UsageView
from .tables import BaseUsageTable, TenantUsageTable, GlobalUsageTable
from .tables import BaseUsageTable, ProjectUsageTable, GlobalUsageTable

View File

@ -1,9 +1,13 @@
from __future__ import division
from calendar import monthrange
from csv import writer, DictWriter
import datetime
import logging
from StringIO import StringIO
from django import template as django_template, VERSION
from django.http.response import HttpResponse
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
@ -27,8 +31,8 @@ def almost_now(input_time):
class BaseUsage(object):
show_terminated = False
def __init__(self, request, tenant_id=None):
self.tenant_id = tenant_id or request.user.tenant_id
def __init__(self, request, project_id=None):
self.project_id = project_id or request.user.tenant_id
self.request = request
self.summary = {}
self.usage_list = []
@ -101,9 +105,9 @@ class BaseUsage(object):
_("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():
for project_usage in self.usage_list:
project_summary = project_usage.get_summary()
for key, value in project_summary.items():
self.summary.setdefault(key, 0)
self.summary[key] += value
@ -130,7 +134,7 @@ class GlobalUsage(BaseUsage):
return api.nova.usage_list(self.request, start, end)
class TenantUsage(BaseUsage):
class ProjectUsage(BaseUsage):
attrs = ('memory_mb', 'vcpus', 'uptime',
'hours', 'local_gb')
@ -139,7 +143,9 @@ class TenantUsage(BaseUsage):
self.show_terminated)
instances = []
terminated_instances = []
usage = api.nova.usage_get(self.request, self.tenant_id, start, end)
usage = api.nova.usage_get(self.request, self.project_id, start, end)
project = api.keystone.tenant_get(self.request, self.project_id)
self.project_name = project.name
# Attribute may not exist if there are no instances
if hasattr(usage, 'server_usages'):
now = self.today
@ -155,3 +161,130 @@ class TenantUsage(BaseUsage):
instances.append(server_usage)
usage.server_usages = instances
return (usage,)
class CsvDataMixin(object):
"""
CSV data Mixin - provides handling for CSV data
.. attribute:: columns
A list of CSV column definitions. If omitted - no column titles
will be shown in the result file. Optional.
"""
def __init__(self):
self.out = StringIO()
super(CsvDataMixin, self).__init__()
if hasattr(self, "columns"):
self.writer = DictWriter(self.out, map(self.encode, self.columns))
self.is_dict = True
else:
self.writer = writer(self.out)
self.is_dict = False
def write_csv_header(self):
if self.is_dict:
try:
self.writer.writeheader()
except AttributeError:
# For Python<2.7
self.writer.writerow(dict(zip(
self.writer.fieldnames,
self.writer.fieldnames)))
def write_csv_row(self, args):
if self.is_dict:
self.writer.writerow(dict(zip(
self.writer.fieldnames, map(self.encode, args))))
else:
self.writer.writerow(map(self.encode, args))
def encode(self, value):
# csv and StringIO cannot work with mixed encodings,
# so encode all with utf-8
return unicode(value).encode('utf-8')
class BaseCsvResponse(CsvDataMixin, HttpResponse):
"""
Base CSV response class. Provides handling of CSV data.
"""
def __init__(self, request, template, context, content_type, **kwargs):
super(BaseCsvResponse, self).__init__()
self['Content-Disposition'] = 'attachment; filename="%s"' % (
kwargs.get("filename", "export.csv"),)
self['Content-Type'] = content_type
self.context = context
self.header = None
if template:
# Display some header info if provided as a template
header_template = django_template.loader.get_template(template)
context = django_template.RequestContext(request, self.context)
self.header = header_template.render(context)
if self.header:
self.out.write(self.encode(self.header))
self.write_csv_header()
for row in self.get_row_data():
self.write_csv_row(row)
self.out.flush()
self.content = self.out.getvalue()
self.out.close()
def get_row_data(self):
raise NotImplementedError("You must define a get_row_data method on %s"
% self.__class__.__name__)
if VERSION >= (1, 5, 0):
from django.http import StreamingHttpResponse
class BaseCsvStreamingResponse(CsvDataMixin, StreamingHttpResponse):
"""
Base CSV Streaming class. Provides streaming response for CSV data.
"""
def __init__(self, request, template, context, content_type, **kwargs):
super(BaseCsvStreamingResponse, self).__init__()
self['Content-Disposition'] = 'attachment; filename="%s"' % (
kwargs.get("filename", "export.csv"),)
self['Content-Type'] = content_type
self.context = context
self.header = None
if template:
# Display some header info if provided as a template
header_template = django_template.loader.get_template(template)
context = django_template.RequestContext(request, self.context)
self.header = header_template.render(context)
self._closable_objects.append(self.out)
self.streaming_content = self.get_content()
def buffer(self):
buf = self.out.getvalue()
self.out.truncate(0)
return buf
def get_content(self):
if self.header:
self.out.write(self.encode(self.header))
self.write_csv_header()
yield self.buffer()
for row in self.get_row_data():
self.write_csv_row(row)
yield self.buffer()
def get_row_data(self):
raise NotImplementedError("You must define a get_row_data method "
"on %s" % self.__class__.__name__)

View File

@ -27,7 +27,7 @@ class BaseUsageTable(tables.DataTable):
class GlobalUsageTable(BaseUsageTable):
tenant = tables.Column('tenant_name', verbose_name=_("Project Name"))
project = tables.Column('project_name', verbose_name=_("Project Name"))
disk_hours = tables.Column('disk_gb_hours',
verbose_name=_("Disk GB Hours"),
filters=(lambda v: floatformat(v, 2),))
@ -38,7 +38,7 @@ class GlobalUsageTable(BaseUsageTable):
class Meta:
name = "global_usage"
verbose_name = _("Usage Summary")
columns = ("tenant", "vcpus", "disk", "memory",
columns = ("project", "vcpus", "disk", "memory",
"hours", "disk_hours")
table_actions = (CSVSummary,)
multi_select = False
@ -52,7 +52,7 @@ def get_instance_link(datum):
return None
class TenantUsageTable(BaseUsageTable):
class ProjectUsageTable(BaseUsageTable):
instance = tables.Column('name',
verbose_name=_("Instance Name"),
link=get_instance_link)
@ -64,7 +64,7 @@ class TenantUsageTable(BaseUsageTable):
return datum.get('instance_id', id(datum))
class Meta:
name = "tenant_usage"
name = "project_usage"
verbose_name = _("Usage Summary")
columns = ("instance", "vcpus", "disk", "memory", "uptime")
table_actions = (CSVSummary,)

View File

@ -28,8 +28,8 @@ class UsageView(tables.DataTableView):
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)
project_id = self.kwargs.get('project_id', self.request.user.tenant_id)
self.usage = self.usage_class(self.request, project_id)
self.usage.summarize(*self.usage.get_date_range())
self.usage.get_quotas()
self.kwargs['usage'] = self.usage
@ -43,12 +43,14 @@ class UsageView(tables.DataTableView):
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'
render_class = self.csv_response_class
response_kwargs.setdefault("filename", "usage.csv")
else:
render_class = self.response_class
resp = render_class(request=self.request,
template=self.get_template_names(),
context=context,
content_type=self.get_content_type(),
**response_kwargs)
return resp