Merge "Improvements in csv export for usage data"
This commit is contained in:
commit
818b92dcf0
@ -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}
|
||||
|
@ -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.
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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'),
|
||||
)
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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.
|
@ -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))
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
|
@ -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') \
|
||||
|
@ -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
|
||||
|
@ -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__)
|
||||
|
@ -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,)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user