From 0c0153a037594c6c98025f698e8593a01ed0ea2d Mon Sep 17 00:00:00 2001 From: Tihomir Trifonov Date: Fri, 21 Jun 2013 13:48:43 +0300 Subject: [PATCH] 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. 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: I18bd70a23b7b8389c7c0f96dbf8794fea5e1e75b --- .../dashboards/admin/instances/tests.py | 7 +- .../overview/templates/overview/usage.csv | 16 +- .../dashboards/admin/overview/tests.py | 27 ++-- .../dashboards/admin/overview/views.py | 32 +++- .../dashboards/admin/projects/urls.py | 4 +- .../dashboards/admin/projects/views.py | 8 +- .../overview/templates/overview/usage.csv | 18 +-- .../dashboards/project/overview/tests.py | 6 +- .../dashboards/project/overview/views.py | 27 +++- .../dashboards/project/volumes/tests.py | 12 +- .../test/api_tests/nova_tests.py | 9 +- .../test/test_data/keystone_data.py | 8 +- .../test/test_data/nova_data.py | 20 ++- openstack_dashboard/test/tests/quotas.py | 14 +- openstack_dashboard/usage/__init__.py | 8 +- openstack_dashboard/usage/base.py | 147 +++++++++++++++++- openstack_dashboard/usage/tables.py | 8 +- openstack_dashboard/usage/views.py | 20 +-- 18 files changed, 302 insertions(+), 89 deletions(-) diff --git a/openstack_dashboard/dashboards/admin/instances/tests.py b/openstack_dashboard/dashboards/admin/instances/tests.py index b6d6ee178a..8e379ef601 100644 --- a/openstack_dashboard/dashboards/admin/instances/tests.py +++ b/openstack_dashboard/dashboards/admin/instances/tests.py @@ -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(), False]) search_opts = {'marker': None, 'paginate': True} diff --git a/openstack_dashboard/dashboards/admin/overview/templates/overview/usage.csv b/openstack_dashboard/dashboards/admin/overview/templates/overview/usage.csv index 686129b203..3749ee2aff 100644 --- a/openstack_dashboard/dashboards/admin/overview/templates/overview/usage.csv +++ b/openstack_dashboard/dashboards/admin/overview/templates/overview/usage.csv @@ -1,10 +1,6 @@ -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 }} - -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 %} +{% 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 }} diff --git a/openstack_dashboard/dashboards/admin/overview/tests.py b/openstack_dashboard/dashboards/admin/overview/tests.py index 965292ab11..0c08529e45 100644 --- a/openstack_dashboard/dashboards/admin/overview/tests.py +++ b/openstack_dashboard/dashboards/admin/overview/tests.py @@ -38,6 +38,7 @@ INDEX_URL = reverse('horizon:project:overview:index') class UsageViewTests(test.BaseAdminViewTests): + @test.create_stubs({api.nova: ('usage_list', 'tenant_absolute_limits', ), api.keystone: ('tenant_list',)}) def test_usage(self): @@ -48,9 +49,9 @@ class UsageViewTests(test.BaseAdminViewTests): api.nova.usage_list(IsA(http.HttpRequest), datetime.datetime(now.year, now.month, 1, 0, 0, 0), Func(usage.almost_now)) \ - .AndReturn([usage_obj]) + .AndReturn([usage_obj]) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ - .AndReturn(self.limits['absolute']) + .AndReturn(self.limits['absolute']) self.mox.ReplayAll() res = self.client.get(reverse('horizon:admin:overview:index')) self.assertTemplateUsed(res, 'admin/overview/usage.html') @@ -73,24 +74,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()] api.keystone.tenant_list(IsA(http.HttpRequest)) \ .AndReturn([self.tenants.list(), False]) 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) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ - .AndReturn(self.limits['absolute']) + .AndReturn(self.limits['absolute']) 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) diff --git a/openstack_dashboard/dashboards/admin/overview/views.py b/openstack_dashboard/dashboards/admin/overview/views.py index 8f4f62ba29..600ab2f179 100644 --- a/openstack_dashboard/dashboards/admin/overview/views.py +++ b/openstack_dashboard/dashboards/admin/overview/views.py @@ -19,18 +19,36 @@ # under the License. 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 +57,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, has_more = api.keystone.tenant_list(self.request) + projects, has_more = 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 diff --git a/openstack_dashboard/dashboards/admin/projects/urls.py b/openstack_dashboard/dashboards/admin/projects/urls.py index 02f84b687a..7afe58ad11 100644 --- a/openstack_dashboard/dashboards/admin/projects/urls.py +++ b/openstack_dashboard/dashboards/admin/projects/urls.py @@ -24,7 +24,7 @@ from django.conf.urls.defaults import url from .views import CreateProjectView from .views import CreateUserView from .views import IndexView -from .views import TenantUsageView +from .views import ProjectUsageView from .views import UpdateProjectView @@ -34,7 +34,7 @@ urlpatterns = patterns('', url(r'^(?P[^/]+)/update/$', UpdateProjectView.as_view(), name='update'), url(r'^(?P[^/]+)/usage/$', - TenantUsageView.as_view(), name='usage'), + ProjectUsageView.as_view(), name='usage'), url(r'^(?P[^/]+)/create_user/$', CreateUserView.as_view(), name='create_user'), ) diff --git a/openstack_dashboard/dashboards/admin/projects/views.py b/openstack_dashboard/dashboards/admin/projects/views.py index 8650f2cb4d..26508e795d 100644 --- a/openstack_dashboard/dashboards/admin/projects/views.py +++ b/openstack_dashboard/dashboards/admin/projects/views.py @@ -129,13 +129,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() diff --git a/openstack_dashboard/dashboards/project/overview/templates/overview/usage.csv b/openstack_dashboard/dashboards/project/overview/templates/overview/usage.csv index 3e2ecd5b2c..b2768b508e 100644 --- a/openstack_dashboard/dashboards/project/overview/templates/overview/usage.csv +++ b/openstack_dashboard/dashboards/project/overview/templates/overview/usage.csv @@ -1,11 +1,7 @@ -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 }} - -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 %} +{% 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 "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 }} diff --git a/openstack_dashboard/dashboards/project/overview/tests.py b/openstack_dashboard/dashboards/project/overview/tests.py index 060c82143b..5cb62e02ce 100644 --- a/openstack_dashboard/dashboards/project/overview/tests.py +++ b/openstack_dashboard/dashboards/project/overview/tests.py @@ -51,7 +51,7 @@ class UsageViewTests(test.TestCase): 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): @@ -91,7 +91,7 @@ class UsageViewTests(test.TestCase): 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)) def test_usage_exception_usage(self): now = timezone.now() @@ -147,4 +147,4 @@ class UsageViewTests(test.TestCase): 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)) diff --git a/openstack_dashboard/dashboards/project/overview/views.py b/openstack_dashboard/dashboards/project/overview/views.py index 32fc3162fd..ae16a3cef3 100644 --- a/openstack_dashboard/dashboards/project/overview/views.py +++ b/openstack_dashboard/dashboards/project/overview/views.py @@ -18,15 +18,38 @@ # License for the specific language governing permissions and limitations # under the License. +from django.template.defaultfilters import floatformat +from django.template.defaultfilters import 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() diff --git a/openstack_dashboard/dashboards/project/volumes/tests.py b/openstack_dashboard/dashboards/project/volumes/tests.py index 2a24d8efe8..2735ad55f5 100644 --- a/openstack_dashboard/dashboards/project/volumes/tests.py +++ b/openstack_dashboard/dashboards/project/volumes/tests.py @@ -678,7 +678,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]) @@ -701,7 +702,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]) @@ -718,13 +720,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() diff --git a/openstack_dashboard/test/api_tests/nova_tests.py b/openstack_dashboard/test/api_tests/nova_tests.py index 6d7da8f778..af45f9ee72 100644 --- a/openstack_dashboard/test/api_tests/nova_tests.py +++ b/openstack_dashboard/test/api_tests/nova_tests.py @@ -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, diff --git a/openstack_dashboard/test/test_data/keystone_data.py b/openstack_dashboard/test/test_data/keystone_data.py index 38f3f67a85..6158f5ec04 100644 --- a/openstack_dashboard/test/test_data/keystone_data.py +++ b/openstack_dashboard/test/test_data/keystone_data.py @@ -181,9 +181,15 @@ def data(TEST): 'name': 'disabled_tenant', 'description': "a disabled test tenant.", 'enabled': False} + tenant_dict_3 = {'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_3) + + TEST.tenants.add(tenant, disabled_tenant, tenant_unicode) TEST.tenant = tenant # Your "current" tenant tomorrow = datetime_safe.datetime.now() + timedelta(days=1) diff --git a/openstack_dashboard/test/test_data/nova_data.py b/openstack_dashboard/test/test_data/nova_data.py index 04b51f45fa..83c9f023fb 100644 --- a/openstack_dashboard/test/test_data/nova_data.py +++ b/openstack_dashboard/test/test_data/nova_data.py @@ -370,6 +370,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", @@ -386,7 +388,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', @@ -444,6 +452,16 @@ def data(TEST): json.loads(USAGE_DATA % usage_vals)) TEST.usages.add(usage_obj) + 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', diff --git a/openstack_dashboard/test/tests/quotas.py b/openstack_dashboard/test/tests/quotas.py index 29061a5dfc..0da699f544 100644 --- a/openstack_dashboard/test/tests/quotas.py +++ b/openstack_dashboard/test/tests/quotas.py @@ -59,6 +59,9 @@ class QuotaTests(test.APITestCase): cinder: ('volume_list', 'volume_snapshot_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)) \ @@ -68,7 +71,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.volume_snapshot_list(IsA(http.HttpRequest)) \ @@ -90,6 +93,9 @@ 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)) \ @@ -99,7 +105,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() @@ -149,6 +155,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) @@ -159,7 +167,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.volume_snapshot_list(IsA(http.HttpRequest)) \ diff --git a/openstack_dashboard/usage/__init__.py b/openstack_dashboard/usage/__init__.py index 2f63c83a47..4aa498d729 100644 --- a/openstack_dashboard/usage/__init__.py +++ b/openstack_dashboard/usage/__init__.py @@ -17,17 +17,17 @@ from openstack_dashboard.usage.base import almost_now from openstack_dashboard.usage.base import BaseUsage from openstack_dashboard.usage.base import GlobalUsage -from openstack_dashboard.usage.base import TenantUsage +from openstack_dashboard.usage.base import ProjectUsage from openstack_dashboard.usage.tables import BaseUsageTable from openstack_dashboard.usage.tables import GlobalUsageTable -from openstack_dashboard.usage.tables import TenantUsageTable +from openstack_dashboard.usage.tables import ProjectUsageTable from openstack_dashboard.usage.views import UsageView assert BaseUsage -assert TenantUsage +assert ProjectUsage assert GlobalUsage assert almost_now assert UsageView assert BaseUsageTable -assert TenantUsageTable +assert ProjectUsageTable assert GlobalUsageTable diff --git a/openstack_dashboard/usage/base.py b/openstack_dashboard/usage/base.py index f89649e1be..5acfd02524 100644 --- a/openstack_dashboard/usage/base.py +++ b/openstack_dashboard/usage/base.py @@ -1,9 +1,15 @@ from __future__ import division from calendar import monthrange +from csv import DictWriter +from csv import writer import datetime import logging +from StringIO import StringIO +from django import template as django_template +from django import VERSION +from django.http import HttpResponse from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -27,8 +33,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 = [] @@ -109,9 +115,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 @@ -138,7 +144,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') @@ -147,7 +153,7 @@ 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) # Attribute may not exist if there are no instances if hasattr(usage, 'server_usages'): now = self.today @@ -163,3 +169,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__) diff --git a/openstack_dashboard/usage/tables.py b/openstack_dashboard/usage/tables.py index c5da8c475b..cca842c360 100644 --- a/openstack_dashboard/usage/tables.py +++ b/openstack_dashboard/usage/tables.py @@ -28,7 +28,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),)) @@ -39,7 +39,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 @@ -53,7 +53,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) @@ -65,7 +65,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,) diff --git a/openstack_dashboard/usage/views.py b/openstack_dashboard/usage/views.py index e0b5063158..702ea35788 100644 --- a/openstack_dashboard/usage/views.py +++ b/openstack_dashboard/usage/views.py @@ -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_limits() 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