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',), @test.create_stubs({api.nova: ('flavor_list', 'server_list',),
api.keystone: ('tenant_list',)}) api.keystone: ('tenant_list',)})
def test_index_options_after_migrate(self): def test_index_options_after_migrate(self):
server = self.servers.first() servers = self.servers.list()
server.status = "VERIFY_RESIZE" server1 = servers[0]
server1.status = "VERIFY_RESIZE"
server2 = servers[2]
server2.status = "VERIFY_RESIZE"
api.keystone.tenant_list(IsA(http.HttpRequest)) \ api.keystone.tenant_list(IsA(http.HttpRequest)) \
.AndReturn(self.tenants.list()) .AndReturn(self.tenants.list())
search_opts = {'marker': None, 'paginate': True} 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" }} {% load i18n %}{% trans "Usage Report For Period" %}:,{{ usage.start|date:"b. d Y" }},{{ usage.end|date:"b. d Y" }}
Active Instances:,{{ usage.summary.instances }} {% trans "Active Instances" %}:,{{ usage.summary.instances }}
CPU-HRs Used:,{{ usage.summary.vcpu_hours|floatformat:2 }} {% trans "CPU-HRs Used" %}:,{{ usage.summary.vcpu_hours|floatformat:2 }}
Total Active RAM (MB):,{{ usage.summary.memory_mb }} {% trans "Total Active RAM (MB)" %}:,{{ usage.summary.memory_mb }}
Total Disk Size:,{{ usage.summary.local_gb }} {% trans "Total Disk Size" %}:,{{ usage.summary.local_gb }}
Total Disk Usage:,{{ usage.summary.disk_gb_hours|floatformat:2 }} {% 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): class UsageViewTests(test.BaseAdminViewTests):
@test.create_stubs({api.nova: ('usage_list',), @test.create_stubs({api.nova: ('usage_list',),
quotas: ('tenant_quota_usages',), quotas: ('tenant_quota_usages',),
api.keystone: ('tenant_list',)}) api.keystone: ('tenant_list',)})
@ -75,24 +76,26 @@ class UsageViewTests(test.BaseAdminViewTests):
api.keystone: ('tenant_list',)}) api.keystone: ('tenant_list',)})
def test_usage_csv(self): def test_usage_csv(self):
now = timezone.now() 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() quota_data = self.quota_usages.first()
api.keystone.tenant_list(IsA(http.HttpRequest)) \ api.keystone.tenant_list(IsA(http.HttpRequest)) \
.AndReturn(self.tenants.list()) .AndReturn(self.tenants.list())
api.nova.usage_list(IsA(http.HttpRequest), api.nova.usage_list(IsA(http.HttpRequest),
datetime.datetime(now.year, now.month, 1, 0, 0, 0), datetime.datetime(now.year, now.month, 1, 0, 0, 0),
Func(usage.almost_now)) \ Func(usage.almost_now)) \
.AndReturn([usage_obj, usage_obj]) .AndReturn(usage_obj)
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data)
self.mox.ReplayAll() self.mox.ReplayAll()
csv_url = reverse('horizon:admin:overview:index') + "?format=csv" csv_url = reverse('horizon:admin:overview:index') + "?format=csv"
res = self.client.get(csv_url) res = self.client.get(csv_url)
self.assertTemplateUsed(res, 'admin/overview/usage.csv') self.assertTemplateUsed(res, 'admin/overview/usage.csv')
self.assertTrue(isinstance(res.context['usage'], usage.GlobalUsage)) self.assertTrue(isinstance(res.context['usage'], usage.GlobalUsage))
hdr = 'Tenant,VCPUs,RamMB,DiskGB,Usage(Hours)' hdr = 'Project Name,VCPUs,Ram (MB),Disk (GB),Usage (Hours)'
row = '%s,%s,%s,%s,%.2f' % (usage_obj.tenant_id, self.assertContains(res, '%s\r\n' % (hdr))
usage_obj.vcpus, for obj in usage_obj:
usage_obj.memory_mb, row = u'{0},{1},{2},{3},{4:.2f}\r\n'.format(obj.project_name,
usage_obj.disk_gb_hours, obj.vcpus,
usage_obj.vcpu_hours) obj.memory_mb,
self.assertContains(res, '%s\n%s\n%s\n' % (hdr, row, row)) 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 # License for the specific language governing permissions and limitations
# under the License. # under the License.
from django import VERSION
from django.conf import settings from django.conf import settings
from django.template.defaultfilters import floatformat
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions from horizon import exceptions
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard import usage 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): class GlobalOverview(usage.UsageView):
table_class = usage.GlobalUsageTable table_class = usage.GlobalUsageTable
usage_class = usage.GlobalUsage usage_class = usage.GlobalUsage
template_name = 'admin/overview/usage.html' template_name = 'admin/overview/usage.html'
csv_response_class = GlobalUsageCsvRenderer
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(GlobalOverview, self).get_context_data(**kwargs) context = super(GlobalOverview, self).get_context_data(**kwargs)
@ -39,17 +58,17 @@ class GlobalOverview(usage.UsageView):
def get_data(self): def get_data(self):
data = super(GlobalOverview, self).get_data() data = super(GlobalOverview, self).get_data()
# Pre-fill tenant names # Pre-fill project names
try: try:
tenants = api.keystone.tenant_list(self.request) projects = api.keystone.tenant_list(self.request)
except: except:
tenants = [] projects = []
exceptions.handle(self.request, exceptions.handle(self.request,
_('Unable to retrieve project list.')) _('Unable to retrieve project list.'))
for instance in data: for instance in data:
tenant = filter(lambda t: t.id == instance.tenant_id, tenants) project = filter(lambda t: t.id == instance.tenant_id, projects)
if tenant: if project:
instance.tenant_name = getattr(tenant[0], "name", None) instance.project_name = getattr(project[0], "name", None)
else: else:
instance.tenant_name = None instance.project_name = None
return data return data

View File

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

View File

@ -116,13 +116,13 @@ class UsersView(tables.MultiTableView):
return context return context
class TenantUsageView(usage.UsageView): class ProjectUsageView(usage.UsageView):
table_class = usage.TenantUsageTable table_class = usage.ProjectUsageTable
usage_class = usage.TenantUsage usage_class = usage.ProjectUsage
template_name = 'admin/projects/usage.html' template_name = 'admin/projects/usage.html'
def get_data(self): def get_data(self):
super(TenantUsageView, self).get_data() super(ProjectUsageView, self).get_data()
return self.usage.get_instances() 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" }} {% load i18n %}{% trans "Usage Report For Period" %}:,{{ usage.start|date:"b. d Y" }},{{ usage.end|date:"b. d Y" }}
Tenant ID:,{{ usage.tenant_id }} {% trans "Project ID" %}:,{{ usage.project_id }}
Total Active VCPUs:,{{ usage.summary.instances }} {% trans "Project Name" %}:,{{ usage.project_name }}
CPU-HRs Used:,{{ usage.summary.vcpu_hours|floatformat:2 }} {% trans "Total Active VCPUs" %}:,{{ usage.summary.instances }}
Total Active Ram (MB):,{{ usage.summary.memory_mb }} {% trans "CPU-HRs Used" %}:,{{ usage.summary.vcpu_hours|floatformat:2 }}
Total Disk Size:,{{ usage.summary.local_gb }} {% trans "Total Active Ram (MB)" %}:,{{ usage.summary.memory_mb }}
Total Disk Usage:,{{ usage.summary.disk_gb_hours|floatformat:2 }} {% 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): class UsageViewTests(test.TestCase):
@test.create_stubs({api.nova: ('usage_get',),
quotas: ('tenant_quota_usages',),
api.keystone: ('tenant_get',)})
def test_usage(self): def test_usage(self):
now = timezone.now() now = timezone.now()
usage_obj = api.nova.NovaUsage(self.usages.first()) usage_obj = api.nova.NovaUsage(self.usages.first())
quota_data = self.quota_usages.first() quota_data = self.quota_usages.first()
self.mox.StubOutWithMock(api.nova, 'usage_get') project = self.tenants.first()
self.mox.StubOutWithMock(quotas, 'tenant_quota_usages')
api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id, api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id,
datetime.datetime(now.year, now.month, 1, 0, 0, 0), datetime.datetime(now.year, now.month, 1, 0, 0, 0),
Func(usage.almost_now)) \ Func(usage.almost_now)) \
.AndReturn(usage_obj) .AndReturn(usage_obj)
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data)
api.keystone.tenant_get(IsA(http.HttpRequest),
project.id).AndReturn(project)
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get(reverse('horizon:project:overview:index')) res = self.client.get(reverse('horizon:project:overview:index'))
self.assertTemplateUsed(res, 'project/overview/usage.html') 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') self.assertContains(res, 'form-horizontal')
def test_unauthorized(self): def test_unauthorized(self):
@ -73,12 +78,14 @@ class UsageViewTests(test.TestCase):
self.assertMessageCount(res, error=1) self.assertMessageCount(res, error=1)
self.assertContains(res, 'Unauthorized:') self.assertContains(res, 'Unauthorized:')
@test.create_stubs({api.nova: ('usage_get',),
quotas: ('tenant_quota_usages',),
api.keystone: ('tenant_get',)})
def test_usage_csv(self): def test_usage_csv(self):
now = timezone.now() now = timezone.now()
usage_obj = api.nova.NovaUsage(self.usages.first()) usage_obj = api.nova.NovaUsage(self.usages.first())
quota_data = self.quota_usages.first() quota_data = self.quota_usages.first()
self.mox.StubOutWithMock(api.nova, 'usage_get') project = self.tenants.first()
self.mox.StubOutWithMock(quotas, 'tenant_quota_usages')
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0)
api.nova.usage_get(IsA(http.HttpRequest), api.nova.usage_get(IsA(http.HttpRequest),
self.tenant.id, self.tenant.id,
@ -86,18 +93,20 @@ class UsageViewTests(test.TestCase):
Func(usage.almost_now)) \ Func(usage.almost_now)) \
.AndReturn(usage_obj) .AndReturn(usage_obj)
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data)
api.keystone.tenant_get(IsA(http.HttpRequest),
project.id).AndReturn(project)
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get(reverse('horizon:project:overview:index') + res = self.client.get(reverse('horizon:project:overview:index') +
"?format=csv") "?format=csv")
self.assertTemplateUsed(res, 'project/overview/usage.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): def test_usage_exception_usage(self):
now = timezone.now() now = timezone.now()
quota_data = self.quota_usages.first() 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) timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0)
api.nova.usage_get(IsA(http.HttpRequest), api.nova.usage_get(IsA(http.HttpRequest),
self.tenant.id, self.tenant.id,
@ -111,11 +120,13 @@ class UsageViewTests(test.TestCase):
self.assertTemplateUsed(res, 'project/overview/usage.html') self.assertTemplateUsed(res, 'project/overview/usage.html')
self.assertEqual(res.context['usage'].usage_list, []) 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): def test_usage_exception_quota(self):
now = timezone.now() now = timezone.now()
usage_obj = api.nova.NovaUsage(self.usages.first()) usage_obj = api.nova.NovaUsage(self.usages.first())
self.mox.StubOutWithMock(api.nova, 'usage_get') project = self.tenants.first()
self.mox.StubOutWithMock(quotas, 'tenant_quota_usages')
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0)
api.nova.usage_get(IsA(http.HttpRequest), api.nova.usage_get(IsA(http.HttpRequest),
self.tenant.id, self.tenant.id,
@ -124,18 +135,22 @@ class UsageViewTests(test.TestCase):
.AndReturn(usage_obj) .AndReturn(usage_obj)
quotas.tenant_quota_usages(IsA(http.HttpRequest))\ quotas.tenant_quota_usages(IsA(http.HttpRequest))\
.AndRaise(self.exceptions.nova) .AndRaise(self.exceptions.nova)
api.keystone.tenant_get(IsA(http.HttpRequest),
project.id).AndReturn(project)
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get(reverse('horizon:project:overview:index')) res = self.client.get(reverse('horizon:project:overview:index'))
self.assertTemplateUsed(res, 'project/overview/usage.html') self.assertTemplateUsed(res, 'project/overview/usage.html')
self.assertEqual(res.context['usage'].quotas, {}) 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): def test_usage_default_tenant(self):
now = timezone.now() now = timezone.now()
usage_obj = api.nova.NovaUsage(self.usages.first()) usage_obj = api.nova.NovaUsage(self.usages.first())
quota_data = self.quota_usages.first() quota_data = self.quota_usages.first()
self.mox.StubOutWithMock(api.nova, 'usage_get') project = self.tenants.first()
self.mox.StubOutWithMock(quotas, 'tenant_quota_usages')
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0)
api.nova.usage_get(IsA(http.HttpRequest), api.nova.usage_get(IsA(http.HttpRequest),
self.tenant.id, self.tenant.id,
@ -143,8 +158,10 @@ class UsageViewTests(test.TestCase):
Func(usage.almost_now)) \ Func(usage.almost_now)) \
.AndReturn(usage_obj) .AndReturn(usage_obj)
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data)
api.keystone.tenant_get(IsA(http.HttpRequest),
project.id).AndReturn(project)
self.mox.ReplayAll() self.mox.ReplayAll()
res = self.client.get(reverse('horizon:project:overview:index')) res = self.client.get(reverse('horizon:project:overview:index'))
self.assertTemplateUsed(res, 'project/overview/usage.html') 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 # License for the specific language governing permissions and limitations
# under the License. # 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 django.views.generic import TemplateView
from openstack_dashboard import usage 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): class ProjectOverview(usage.UsageView):
table_class = usage.TenantUsageTable table_class = usage.ProjectUsageTable
usage_class = usage.TenantUsage usage_class = usage.ProjectUsage
template_name = 'project/overview/usage.html' template_name = 'project/overview/usage.html'
csv_response_class = ProjectUsageCsvRenderer
def get_data(self): def get_data(self):
super(ProjectOverview, self).get_data() 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',)}) @test.create_stubs({cinder: ('volume_get',), api.nova: ('server_list',)})
def test_edit_attachments(self): def test_edit_attachments(self):
volume = self.volumes.first() 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) cinder.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume)
api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False]) 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 settings.OPENSTACK_HYPERVISOR_FEATURES['can_set_mount_point'] = False
volume = self.volumes.first() 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) cinder.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume)
api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False]) api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False])
@ -649,13 +651,15 @@ class VolumeViewTests(test.TestCase):
@test.create_stubs({cinder: ('volume_get',), @test.create_stubs({cinder: ('volume_get',),
api.nova: ('server_get', 'server_list',)}) api.nova: ('server_get', 'server_list',)})
def test_edit_attachments_attached_volume(self): 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] volume = self.volumes.list()[0]
cinder.volume_get(IsA(http.HttpRequest), volume.id) \ cinder.volume_get(IsA(http.HttpRequest), volume.id) \
.AndReturn(volume) .AndReturn(volume)
api.nova.server_list(IsA(http.HttpRequest)) \ api.nova.server_list(IsA(http.HttpRequest)) \
.AndReturn([self.servers.list(), False]) .AndReturn([servers, False])
self.mox.ReplayAll() self.mox.ReplayAll()

View File

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

View File

@ -139,9 +139,14 @@ def data(TEST):
'name': 'disabled_tenant', 'name': 'disabled_tenant',
'description': "a disabled test tenant.", 'description': "a disabled test tenant.",
'enabled': False} '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) tenant = tenants.Tenant(tenants.TenantManager, tenant_dict)
disabled_tenant = tenants.Tenant(tenants.TenantManager, tenant_dict_2) 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 TEST.tenant = tenant # Your "current" tenant
tomorrow = datetime_safe.datetime.now() + timedelta(days=1) tomorrow = datetime_safe.datetime.now() + timedelta(days=1)

View File

@ -361,6 +361,8 @@ def data(TEST):
TEST.limits = limits TEST.limits = limits
# Servers # Servers
tenant3 = TEST.tenants.list()[2]
vals = {"host": "http://nova.example.com:8774", vals = {"host": "http://nova.example.com:8774",
"name": "server_1", "name": "server_1",
"status": "ACTIVE", "status": "ACTIVE",
@ -377,7 +379,13 @@ def data(TEST):
"server_id": "2"}) "server_id": "2"})
server_2 = servers.Server(servers.ServerManager(None), server_2 = servers.Server(servers.ServerManager(None),
json.loads(SERVER_DATA % vals)['server']) 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 # VNC Console Data
console = {u'console': {u'url': u'http://example.com:6080/vnc_auto.html', 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)) json.loads(USAGE_DATA % usage_vals))
TEST.usages.add(usage_obj) 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), volume_snapshot = vol_snaps.Snapshot(vol_snaps.SnapshotManager(None),
{'id': '40f3fabf-3613-4f5e-90e5-6c9a08333fc3', {'id': '40f3fabf-3613-4f5e-90e5-6c9a08333fc3',
'display_name': 'test snapshot', 'display_name': 'test snapshot',

View File

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

View File

@ -14,6 +14,6 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from .base import BaseUsage, TenantUsage, GlobalUsage, almost_now from .base import BaseUsage, ProjectUsage, GlobalUsage, almost_now
from .views import UsageView 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 __future__ import division
from calendar import monthrange from calendar import monthrange
from csv import writer, DictWriter
import datetime import datetime
import logging 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.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
@ -27,8 +31,8 @@ def almost_now(input_time):
class BaseUsage(object): class BaseUsage(object):
show_terminated = False show_terminated = False
def __init__(self, request, tenant_id=None): def __init__(self, request, project_id=None):
self.tenant_id = tenant_id or request.user.tenant_id self.project_id = project_id or request.user.tenant_id
self.request = request self.request = request
self.summary = {} self.summary = {}
self.usage_list = [] self.usage_list = []
@ -101,9 +105,9 @@ class BaseUsage(object):
_("You are viewing data for the future, " _("You are viewing data for the future, "
"which may or may not exist.")) "which may or may not exist."))
for tenant_usage in self.usage_list: for project_usage in self.usage_list:
tenant_summary = tenant_usage.get_summary() project_summary = project_usage.get_summary()
for key, value in tenant_summary.items(): for key, value in project_summary.items():
self.summary.setdefault(key, 0) self.summary.setdefault(key, 0)
self.summary[key] += value self.summary[key] += value
@ -130,7 +134,7 @@ class GlobalUsage(BaseUsage):
return api.nova.usage_list(self.request, start, end) return api.nova.usage_list(self.request, start, end)
class TenantUsage(BaseUsage): class ProjectUsage(BaseUsage):
attrs = ('memory_mb', 'vcpus', 'uptime', attrs = ('memory_mb', 'vcpus', 'uptime',
'hours', 'local_gb') 'hours', 'local_gb')
@ -139,7 +143,9 @@ class TenantUsage(BaseUsage):
self.show_terminated) self.show_terminated)
instances = [] instances = []
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 # Attribute may not exist if there are no instances
if hasattr(usage, 'server_usages'): if hasattr(usage, 'server_usages'):
now = self.today now = self.today
@ -155,3 +161,130 @@ class TenantUsage(BaseUsage):
instances.append(server_usage) instances.append(server_usage)
usage.server_usages = instances usage.server_usages = instances
return (usage,) 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): 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', disk_hours = tables.Column('disk_gb_hours',
verbose_name=_("Disk GB Hours"), verbose_name=_("Disk GB Hours"),
filters=(lambda v: floatformat(v, 2),)) filters=(lambda v: floatformat(v, 2),))
@ -38,7 +38,7 @@ class GlobalUsageTable(BaseUsageTable):
class Meta: class Meta:
name = "global_usage" name = "global_usage"
verbose_name = _("Usage Summary") verbose_name = _("Usage Summary")
columns = ("tenant", "vcpus", "disk", "memory", columns = ("project", "vcpus", "disk", "memory",
"hours", "disk_hours") "hours", "disk_hours")
table_actions = (CSVSummary,) table_actions = (CSVSummary,)
multi_select = False multi_select = False
@ -52,7 +52,7 @@ def get_instance_link(datum):
return None return None
class TenantUsageTable(BaseUsageTable): class ProjectUsageTable(BaseUsageTable):
instance = tables.Column('name', instance = tables.Column('name',
verbose_name=_("Instance Name"), verbose_name=_("Instance Name"),
link=get_instance_link) link=get_instance_link)
@ -64,7 +64,7 @@ class TenantUsageTable(BaseUsageTable):
return datum.get('instance_id', id(datum)) return datum.get('instance_id', id(datum))
class Meta: class Meta:
name = "tenant_usage" name = "project_usage"
verbose_name = _("Usage Summary") verbose_name = _("Usage Summary")
columns = ("instance", "vcpus", "disk", "memory", "uptime") columns = ("instance", "vcpus", "disk", "memory", "uptime")
table_actions = (CSVSummary,) table_actions = (CSVSummary,)

View File

@ -28,8 +28,8 @@ class UsageView(tables.DataTableView):
return "text/html" return "text/html"
def get_data(self): def get_data(self):
tenant_id = self.kwargs.get('tenant_id', self.request.user.tenant_id) project_id = self.kwargs.get('project_id', self.request.user.tenant_id)
self.usage = self.usage_class(self.request, tenant_id) self.usage = self.usage_class(self.request, project_id)
self.usage.summarize(*self.usage.get_date_range()) self.usage.summarize(*self.usage.get_date_range())
self.usage.get_quotas() self.usage.get_quotas()
self.kwargs['usage'] = self.usage self.kwargs['usage'] = self.usage
@ -43,12 +43,14 @@ class UsageView(tables.DataTableView):
return context return context
def render_to_response(self, context, **response_kwargs): 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': if self.request.GET.get('format', 'html') == 'csv':
resp['Content-Disposition'] = 'attachment; filename=usage.csv' render_class = self.csv_response_class
resp['Content-Type'] = 'text/csv' 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 return resp