From cdcd8e3df6ee143cc96b5e2a2d849e9ae8e86148 Mon Sep 17 00:00:00 2001 From: Gabriel Hurley Date: Tue, 23 Oct 2012 22:25:18 -0700 Subject: [PATCH] Enable quota data from multiple sources. Now that there are multiple projects with quota data (cinder, quantum) we need to accommodate that data being aggregated in a centralized fashion. This commit takes care of that for nova + cinder, and paves the way for quantum later. Fixes bug 1070022. Change-Id: Ifc68c2dc681b2a7b4e7787e0b1a7dca1a970fc36 --- horizon/static/horizon/js/horizon.quota.js | 2 +- horizon/utils/memoized.py | 3 + openstack_dashboard/api/__init__.py | 1 + openstack_dashboard/api/base.py | 57 ++++++ openstack_dashboard/api/cinder.py | 115 +++++++++++ openstack_dashboard/api/nova.py | 188 +++--------------- .../dashboards/admin/overview/tests.py | 19 +- .../dashboards/admin/projects/tests.py | 104 +++++----- .../dashboards/admin/projects/views.py | 10 +- .../dashboards/admin/projects/workflows.py | 32 ++- .../dashboards/admin/quotas/views.py | 2 +- .../access_and_security/floating_ips/views.py | 3 +- .../volume_snapshots/tables.py | 3 +- .../volume_snapshots/tabs.py | 6 +- .../dashboards/project/instances/tests.py | 114 +++++------ .../dashboards/project/instances/views.py | 4 +- .../dashboards/project/instances/workflows.py | 8 +- .../dashboards/project/overview/tests.py | 39 ++-- .../dashboards/project/volumes/forms.py | 14 +- .../dashboards/project/volumes/tables.py | 4 +- .../dashboards/project/volumes/tabs.py | 7 +- .../dashboards/project/volumes/tests.py | 57 +++--- .../dashboards/project/volumes/views.py | 3 +- .../test/api_tests/nova_tests.py | 52 +---- .../test/test_data/nova_data.py | 30 +-- openstack_dashboard/test/tests/quotas.py | 69 +++++++ openstack_dashboard/usage/base.py | 3 +- openstack_dashboard/usage/quotas.py | 108 ++++++++++ 28 files changed, 626 insertions(+), 431 deletions(-) create mode 100644 openstack_dashboard/api/cinder.py create mode 100644 openstack_dashboard/test/tests/quotas.py create mode 100644 openstack_dashboard/usage/quotas.py diff --git a/horizon/static/horizon/js/horizon.quota.js b/horizon/static/horizon/js/horizon.quota.js index 0aaa3cad3..7e9975634 100644 --- a/horizon/static/horizon/js/horizon.quota.js +++ b/horizon/static/horizon/js/horizon.quota.js @@ -140,7 +140,7 @@ horizon.Quota = { element.removeClass('progress_bar_over'); } - element.animate({width: update_width + "%"}, 300); + element.animate({width: parseInt(update_width, 10) + "%"}, 300); }, /* diff --git a/horizon/utils/memoized.py b/horizon/utils/memoized.py index 0c260ddc2..c3d7e7c56 100644 --- a/horizon/utils/memoized.py +++ b/horizon/utils/memoized.py @@ -45,3 +45,6 @@ class memoized(object): def __get__(self, obj, objtype): '''Support instance methods.''' return functools.partial(self.__call__, obj) + + def __str__(self): + return str(self.func) diff --git a/openstack_dashboard/api/__init__.py b/openstack_dashboard/api/__init__.py index 65383e366..2ffdfe660 100644 --- a/openstack_dashboard/api/__init__.py +++ b/openstack_dashboard/api/__init__.py @@ -37,3 +37,4 @@ from openstack_dashboard.api.keystone import * from openstack_dashboard.api.nova import * from openstack_dashboard.api.swift import * from openstack_dashboard.api.quantum import * +from openstack_dashboard.api.cinder import * diff --git a/openstack_dashboard/api/base.py b/openstack_dashboard/api/base.py index 15f9f9eeb..3dfa02ce2 100644 --- a/openstack_dashboard/api/base.py +++ b/openstack_dashboard/api/base.py @@ -18,6 +18,7 @@ # License for the specific language governing permissions and limitations # under the License. +from collections import Sequence import logging from django.conf import settings @@ -92,6 +93,53 @@ class APIDictWrapper(object): return default +class Quota(object): + """Wrapper for individual limits in a quota.""" + def __init__(self, name, limit): + self.name = name + self.limit = limit + + def __repr__(self): + return "" % (self.name, self.limit) + + +class QuotaSet(Sequence): + """ + Wrapper for client QuotaSet objects which turns the individual quotas + into Quota objects for easier handling/iteration. + + `QuotaSet` objects support a mix of `list` and `dict` methods; you can use + the bracket notiation (`qs["my_quota"] = 0`) to add new quota values, and + use the `get` method to retrieve a specific quota, but otherwise it + behaves much like a list or tuple, particularly in supporting iteration. + """ + def __init__(self, apiresource=None): + self.items = [] + if apiresource: + for k, v in apiresource._info.items(): + if k == 'id': + continue + self[k] = v + + def __setitem__(self, k, v): + v = int(v) if v is not None else v + q = Quota(k, v) + self.items.append(q) + + def __getitem__(self, index): + return self.items[index] + + def __len__(self): + return len(self.items) + + def __repr__(self): + return repr(self.items) + + def get(self, key, default=None): + match = [quota for quota in self.items if quota.name == key] + return match.pop() if len(match) else Quota(key, default) + + def get_service_from_catalog(catalog, service_type): if catalog: for service in catalog: @@ -116,3 +164,12 @@ def url_for(request, service_type, admin=False, endpoint_type=None): raise exceptions.ServiceCatalogException(service_type) else: raise exceptions.ServiceCatalogException(service_type) + + +def is_service_enabled(request, service_type, service_name=None): + service = get_service_from_catalog(request.user.service_catalog, + service_type) + if service and service_name: + return service['name'] == service_name + else: + return service is not None diff --git a/openstack_dashboard/api/cinder.py b/openstack_dashboard/api/cinder.py new file mode 100644 index 000000000..d0beae649 --- /dev/null +++ b/openstack_dashboard/api/cinder.py @@ -0,0 +1,115 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 Openstack, LLC +# Copyright 2012 Nebula, Inc. +# Copyright (c) 2012 X.commerce, a business unit of eBay Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +import logging + +from django.conf import settings +from django.utils.translation import ugettext as _ + +from cinderclient.v1 import client as cinder_client + +from openstack_dashboard.api.base import url_for +from openstack_dashboard.api import nova, QuotaSet + + +LOG = logging.getLogger(__name__) + + +# API static values +VOLUME_STATE_AVAILABLE = "available" + + +def cinderclient(request): + insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) + LOG.debug('cinderclient connection created using token "%s" and url "%s"' % + (request.user.token.id, url_for(request, 'volume'))) + c = cinder_client.Client(request.user.username, + request.user.token.id, + project_id=request.user.tenant_id, + auth_url=url_for(request, 'volume'), + insecure=insecure) + c.client.auth_token = request.user.token.id + c.client.management_url = url_for(request, 'volume') + return c + + +def volume_list(request, search_opts=None): + """ + To see all volumes in the cloud as an admin you can pass in a special + search option: {'all_tenants': 1} + """ + return cinderclient(request).volumes.list(search_opts=search_opts) + + +def volume_get(request, volume_id): + volume_data = cinderclient(request).volumes.get(volume_id) + + for attachment in volume_data.attachments: + if "server_id" in attachment: + instance = nova.server_get(request, attachment['server_id']) + attachment['instance_name'] = instance.name + else: + # Nova volume can occasionally send back error'd attachments + # the lack a server_id property; to work around that we'll + # give the attached instance a generic name. + attachment['instance_name'] = _("Unknown instance") + return volume_data + + +def volume_create(request, size, name, description, snapshot_id=None): + return cinderclient(request).volumes.create(size, display_name=name, + display_description=description, snapshot_id=snapshot_id) + + +def volume_delete(request, volume_id): + return cinderclient(request).volumes.delete(volume_id) + + +def volume_snapshot_get(request, snapshot_id): + return cinderclient(request).volume_snapshots.get(snapshot_id) + + +def volume_snapshot_list(request): + return cinderclient(request).volume_snapshots.list() + + +def volume_snapshot_create(request, volume_id, name, description): + return cinderclient(request).volume_snapshots.create( + volume_id, display_name=name, display_description=description) + + +def volume_snapshot_delete(request, snapshot_id): + return cinderclient(request).volume_snapshots.delete(snapshot_id) + + +def tenant_quota_get(request, tenant_id): + return QuotaSet(cinderclient(request).quotas.get(tenant_id)) + + +def tenant_quota_update(request, tenant_id, **kwargs): + return cinderclient(request).quotas.update(tenant_id, **kwargs) + + +def default_quota_get(request, tenant_id): + return QuotaSet(cinderclient(request).quotas.defaults(tenant_id)) diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 088231470..3db80395e 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -27,17 +27,14 @@ import logging from django.conf import settings from django.utils.translation import ugettext as _ -from cinderclient.v1 import client as cinder_client - from novaclient.v1_1 import client as nova_client from novaclient.v1_1 import security_group_rules as nova_rules from novaclient.v1_1.security_groups import SecurityGroup as NovaSecurityGroup from novaclient.v1_1.servers import REBOOT_HARD -from horizon import exceptions from horizon.utils.memoized import memoized -from openstack_dashboard.api.base import (APIResourceWrapper, +from openstack_dashboard.api.base import (APIResourceWrapper, QuotaSet, APIDictWrapper, url_for) @@ -56,32 +53,6 @@ class VNCConsole(APIDictWrapper): _attrs = ['url', 'type'] -class Quota(object): - """Wrapper for individual limits in a quota.""" - def __init__(self, name, limit): - self.name = name - self.limit = limit - - def __repr__(self): - return "" % (self.name, self.limit) - - -class QuotaSet(object): - """Wrapper for novaclient.quotas.QuotaSet objects which wraps the - individual quotas inside Quota objects. - """ - def __init__(self, apiresource): - self.items = [] - for k in apiresource._info.keys(): - if k in ['id']: - continue - limit = apiresource._info[k] - v = int(limit) if limit is not None else limit - q = Quota(k, v) - self.items.append(q) - setattr(self, k, v) - - class Server(APIResourceWrapper): """Simple wrapper around novaclient.server.Server @@ -117,7 +88,7 @@ class Server(APIResourceWrapper): novaclient(self.request).servers.reboot(self.id, hardness) -class Usage(APIResourceWrapper): +class NovaUsage(APIResourceWrapper): """Simple wrapper around contrib/simple_usage.py.""" _attrs = ['start', 'server_usages', 'stop', 'tenant_id', 'total_local_gb_usage', 'total_memory_mb_usage', @@ -210,20 +181,6 @@ def novaclient(request): return c -def cinderclient(request): - insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) - LOG.debug('cinderclient connection created using token "%s" and url "%s"' % - (request.user.token.id, url_for(request, 'volume'))) - c = cinder_client.Client(request.user.username, - request.user.token.id, - project_id=request.user.tenant_id, - auth_url=url_for(request, 'volume'), - insecure=insecure) - c.client.auth_token = request.user.token.id - c.client.management_url = url_for(request, 'volume') - return c - - def server_vnc_console(request, instance_id, console_type='novnc'): return VNCConsole(novaclient(request).servers.get_vnc_console(instance_id, console_type)['console']) @@ -408,68 +365,17 @@ def tenant_quota_update(request, tenant_id, **kwargs): novaclient(request).quotas.update(tenant_id, **kwargs) -def tenant_quota_defaults(request, tenant_id): +def default_quota_get(request, tenant_id): return QuotaSet(novaclient(request).quotas.defaults(tenant_id)) def usage_get(request, tenant_id, start, end): - return Usage(novaclient(request).usage.get(tenant_id, start, end)) + return NovaUsage(novaclient(request).usage.get(tenant_id, start, end)) def usage_list(request, start, end): - return [Usage(u) for u in novaclient(request).usage.list(start, end, True)] - - -@memoized -def tenant_quota_usages(request): - """ - Builds a dictionary of current usage against quota for the current - project. - """ - instances = server_list(request) - floating_ips = tenant_floating_ip_list(request) - quotas = tenant_quota_get(request, request.user.tenant_id) - flavors = dict([(f.id, f) for f in flavor_list(request)]) - volumes = volume_list(request) - - usages = {'instances': {'flavor_fields': [], 'used': len(instances)}, - 'cores': {'flavor_fields': ['vcpus'], 'used': 0}, - 'gigabytes': {'used': sum([int(v.size) for v in volumes]), - 'flavor_fields': []}, - 'volumes': {'used': len(volumes), 'flavor_fields': []}, - 'ram': {'flavor_fields': ['ram'], 'used': 0}, - 'floating_ips': {'flavor_fields': [], 'used': len(floating_ips)}} - - for usage in usages: - for instance in instances: - used_flavor = instance.flavor['id'] - if used_flavor not in flavors: - try: - flavors[used_flavor] = flavor_get(request, used_flavor) - except: - flavors[used_flavor] = {} - exceptions.handle(request, ignore=True) - for flavor_field in usages[usage]['flavor_fields']: - instance_flavor = flavors[used_flavor] - usages[usage]['used'] += getattr(instance_flavor, - flavor_field, - 0) - - usages[usage]['quota'] = getattr(quotas, usage) - - if usages[usage]['quota'] is None: - usages[usage]['quota'] = float("inf") - usages[usage]['available'] = float("inf") - elif type(usages[usage]['quota']) is str: - usages[usage]['quota'] = int(usages[usage]['quota']) - else: - if type(usages[usage]['used']) is str: - usages[usage]['used'] = int(usages[usage]['used']) - - usages[usage]['available'] = usages[usage]['quota'] - \ - usages[usage]['used'] - - return usages + return [NovaUsage(u) for u in + novaclient(request).usage.list(start, end, True)] def security_group_list(request): @@ -510,30 +416,28 @@ def virtual_interfaces_list(request, instance_id): return novaclient(request).virtual_interfaces.list(instance_id) -def volume_list(request, search_opts=None): - """ - To see all volumes in the cloud as an admin you can pass in a special - search option: {'all_tenants': 1} - """ - return cinderclient(request).volumes.list(search_opts=search_opts) +def get_x509_credentials(request): + return novaclient(request).certs.create() -def volume_get(request, volume_id): - volume_data = cinderclient(request).volumes.get(volume_id) - - for attachment in volume_data.attachments: - if "server_id" in attachment: - instance = server_get(request, attachment['server_id']) - attachment['instance_name'] = instance.name - else: - # Nova volume can occasionally send back error'd attachments - # the lack a server_id property; to work around that we'll - # give the attached instance a generic name. - attachment['instance_name'] = _("Unknown instance") - return volume_data +def get_x509_root_certificate(request): + return novaclient(request).certs.get() -def volume_instance_list(request, instance_id): +def instance_volume_attach(request, volume_id, instance_id, device): + return novaclient(request).volumes.create_server_volume(instance_id, + volume_id, + device) + + +def instance_volume_detach(request, instance_id, att_id): + return novaclient(request).volumes.delete_server_volume(instance_id, + att_id) + + +def instance_volumes_list(request, instance_id): + from openstack_dashboard.api.cinder import cinderclient + volumes = novaclient(request).volumes.get_server_volumes(instance_id) for volume in volumes: @@ -541,47 +445,3 @@ def volume_instance_list(request, instance_id): volume.name = volume_data.display_name return volumes - - -def volume_create(request, size, name, description, snapshot_id=None): - return cinderclient(request).volumes.create(size, display_name=name, - display_description=description, snapshot_id=snapshot_id) - - -def volume_delete(request, volume_id): - cinderclient(request).volumes.delete(volume_id) - - -def volume_attach(request, volume_id, instance_id, device): - return novaclient(request).volumes.create_server_volume(instance_id, - volume_id, - device) - - -def volume_detach(request, instance_id, att_id): - novaclient(request).volumes.delete_server_volume(instance_id, att_id) - - -def volume_snapshot_get(request, snapshot_id): - return cinderclient(request).volume_snapshots.get(snapshot_id) - - -def volume_snapshot_list(request): - return cinderclient(request).volume_snapshots.list() - - -def volume_snapshot_create(request, volume_id, name, description): - return cinderclient(request).volume_snapshots.create( - volume_id, display_name=name, display_description=description) - - -def volume_snapshot_delete(request, snapshot_id): - cinderclient(request).volume_snapshots.delete(snapshot_id) - - -def get_x509_credentials(request): - return novaclient(request).certs.create() - - -def get_x509_root_certificate(request): - return novaclient(request).certs.get() diff --git a/openstack_dashboard/dashboards/admin/overview/tests.py b/openstack_dashboard/dashboards/admin/overview/tests.py index bdbce47ef..c3f06df3b 100644 --- a/openstack_dashboard/dashboards/admin/overview/tests.py +++ b/openstack_dashboard/dashboards/admin/overview/tests.py @@ -29,8 +29,9 @@ from mox import IsA, Func from horizon.templatetags.sizeformat import mbformat from openstack_dashboard import api -from openstack_dashboard.test import helpers as test from openstack_dashboard import usage +from openstack_dashboard.test import helpers as test +from openstack_dashboard.usage import quotas INDEX_URL = reverse('horizon:project:overview:index') @@ -38,19 +39,19 @@ INDEX_URL = reverse('horizon:project:overview:index') class UsageViewTests(test.BaseAdminViewTests): @test.create_stubs({api: ('usage_list',), - api.nova: ('tenant_quota_usages',), + quotas: ('tenant_quota_usages',), api.keystone: ('tenant_list',)}) def test_usage(self): now = timezone.now() - usage_obj = api.nova.Usage(self.usages.first()) - quotas = self.quota_usages.first() + usage_obj = api.nova.NovaUsage(self.usages.first()) + quota_data = self.quota_usages.first() api.keystone.tenant_list(IsA(http.HttpRequest), admin=True) \ .AndReturn(self.tenants.list()) api.usage_list(IsA(http.HttpRequest), datetime.datetime(now.year, now.month, 1, 0, 0, 0), Func(usage.almost_now)) \ .AndReturn([usage_obj]) - api.nova.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quotas) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data) self.mox.ReplayAll() res = self.client.get(reverse('horizon:admin:overview:index')) self.assertTemplateUsed(res, 'admin/overview/usage.html') @@ -70,19 +71,19 @@ class UsageViewTests(test.BaseAdminViewTests): usage_obj.total_local_gb_usage)) @test.create_stubs({api: ('usage_list',), - api.nova: ('tenant_quota_usages',), + quotas: ('tenant_quota_usages',), api.keystone: ('tenant_list',)}) def test_usage_csv(self): now = timezone.now() - usage_obj = api.nova.Usage(self.usages.first()) - quotas = self.quota_usages.first() + usage_obj = api.nova.NovaUsage(self.usages.first()) + quota_data = self.quota_usages.first() api.keystone.tenant_list(IsA(http.HttpRequest), admin=True) \ .AndReturn(self.tenants.list()) api.usage_list(IsA(http.HttpRequest), datetime.datetime(now.year, now.month, 1, 0, 0, 0), Func(usage.almost_now)) \ .AndReturn([usage_obj]) - api.nova.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quotas) + 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) diff --git a/openstack_dashboard/dashboards/admin/projects/tests.py b/openstack_dashboard/dashboards/admin/projects/tests.py index a7d264125..0fb094b93 100644 --- a/openstack_dashboard/dashboards/admin/projects/tests.py +++ b/openstack_dashboard/dashboards/admin/projects/tests.py @@ -21,6 +21,7 @@ from mox import IsA from openstack_dashboard import api from openstack_dashboard.test import helpers as test +from openstack_dashboard.usage import quotas from .workflows import CreateProject, UpdateProject from .views import QUOTA_FIELDS @@ -55,7 +56,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): def _get_quota_info(self, quota): quota_data = {} for field in QUOTA_FIELDS: - quota_data[field] = int(getattr(quota, field, None)) + quota_data[field] = int(quota.get(field).limit) return quota_data def _get_workflow_data(self, project, quota): @@ -64,18 +65,17 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): project_info.update(quota_data) return project_info - @test.create_stubs({api: ('tenant_quota_defaults', - 'get_default_role',), - api.keystone: ('user_list', - 'role_list',)}) + @test.create_stubs({api: ('get_default_role',), + quotas: ('get_default_quota_data',), + api.keystone: ('user_list', + 'role_list',)}) def test_add_project_get(self): quota = self.quotas.first() default_role = self.roles.first() users = self.users.list() roles = self.roles.list() - api.tenant_quota_defaults(IsA(http.HttpRequest), self.tenant.id) \ - .AndReturn(quota) + quotas.get_default_quota_data(IsA(http.HttpRequest)).AndReturn(quota) # init api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role) @@ -93,20 +93,21 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): self.assertEqual(res.context['workflow'].name, CreateProject.name) step = workflow.get_step("createprojectinfoaction") - self.assertEqual(step.action.initial['ram'], quota.ram) + self.assertEqual(step.action.initial['ram'], quota.get('ram').limit) self.assertEqual(step.action.initial['injected_files'], - quota.injected_files) + quota.get('injected_files').limit) self.assertQuerysetEqual(workflow.steps, ['', '', '']) @test.create_stubs({api: ('get_default_role', - 'tenant_quota_defaults', 'add_tenant_user_role',), api.keystone: ('tenant_create', 'user_list', 'role_list'), + quotas: ('get_default_quota_data',), + api.cinder: ('tenant_quota_update',), api.nova: ('tenant_quota_update',)}) def test_add_project_post(self): project = self.tenants.first() @@ -116,8 +117,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): roles = self.roles.list() # init - api.tenant_quota_defaults(IsA(http.HttpRequest), self.tenant.id) \ - .AndReturn(quota) + quotas.get_default_quota_data(IsA(http.HttpRequest)).AndReturn(quota) api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users) @@ -159,8 +159,8 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, INDEX_URL) - @test.create_stubs({api: ('tenant_quota_defaults', - 'get_default_role',), + @test.create_stubs({api: ('get_default_role',), + quotas: ('get_default_quota_data',), api.keystone: ('user_list', 'role_list',)}) def test_add_project_quota_defaults_error(self): @@ -169,8 +169,8 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): roles = self.roles.list() # init - api.tenant_quota_defaults(IsA(http.HttpRequest), self.tenant.id) \ - .AndRaise(self.exceptions.nova) + quotas.get_default_quota_data(IsA(http.HttpRequest)) \ + .AndRaise(self.exceptions.nova) api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users) @@ -184,8 +184,8 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): self.assertTemplateUsed(res, 'admin/projects/create.html') self.assertContains(res, "Unable to retrieve default quota values") - @test.create_stubs({api: ('get_default_role', - 'tenant_quota_defaults',), + @test.create_stubs({api: ('get_default_role',), + quotas: ('get_default_quota_data',), api.keystone: ('tenant_create', 'user_list', 'role_list',)}) @@ -197,8 +197,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): roles = self.roles.list() # init - api.tenant_quota_defaults(IsA(http.HttpRequest), self.tenant.id) \ - .AndReturn(quota) + quotas.get_default_quota_data(IsA(http.HttpRequest)).AndReturn(quota) api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users) @@ -224,11 +223,11 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_stubs({api: ('get_default_role', - 'tenant_quota_defaults', 'add_tenant_user_role',), api.keystone: ('tenant_create', 'user_list', 'role_list'), + quotas: ('get_default_quota_data',), api.nova: ('tenant_quota_update',)}) def test_add_project_quota_update_error(self): project = self.tenants.first() @@ -238,8 +237,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): roles = self.roles.list() # init - api.tenant_quota_defaults(IsA(http.HttpRequest), self.tenant.id) \ - .AndReturn(quota) + quotas.get_default_quota_data(IsA(http.HttpRequest)).AndReturn(quota) api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users) @@ -283,11 +281,11 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_stubs({api: ('get_default_role', - 'tenant_quota_defaults', 'add_tenant_user_role',), api.keystone: ('tenant_create', 'user_list', 'role_list',), + quotas: ('get_default_quota_data',), api.nova: ('tenant_quota_update',)}) def test_add_project_user_update_error(self): project = self.tenants.first() @@ -297,8 +295,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): roles = self.roles.list() # init - api.tenant_quota_defaults(IsA(http.HttpRequest), self.tenant.id) \ - .AndReturn(quota) + quotas.get_default_quota_data(IsA(http.HttpRequest)).AndReturn(quota) api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users) @@ -343,8 +340,8 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, INDEX_URL) - @test.create_stubs({api: ('get_default_role', - 'tenant_quota_defaults',), + @test.create_stubs({api: ('get_default_role',), + quotas: ('get_default_quota_data',), api.keystone: ('user_list', 'role_list',)}) def test_add_project_missing_field_error(self): @@ -355,8 +352,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests): roles = self.roles.list() # init - api.tenant_quota_defaults(IsA(http.HttpRequest), self.tenant.id) \ - .AndReturn(quota) + quotas.get_default_quota_data(IsA(http.HttpRequest)).AndReturn(quota) api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users) @@ -380,13 +376,13 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): def _get_quota_info(self, quota): quota_data = {} for field in QUOTA_FIELDS: - quota_data[field] = int(getattr(quota, field, None)) + quota_data[field] = int(quota.get(field).limit) return quota_data @test.create_stubs({api: ('get_default_role', 'roles_for_user', - 'tenant_get', - 'tenant_quota_get',), + 'tenant_get',), + quotas: ('get_tenant_quota_data',), api.keystone: ('user_list', 'role_list',)}) def test_update_project_get(self): @@ -398,8 +394,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \ .AndReturn(project) - api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \ - .AndReturn(quota) + quotas.get_tenant_quota_data(IsA(http.HttpRequest)).AndReturn(quota) api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users) @@ -422,9 +417,9 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): self.assertEqual(res.context['workflow'].name, UpdateProject.name) step = workflow.get_step("update_info") - self.assertEqual(step.action.initial['ram'], quota.ram) + self.assertEqual(step.action.initial['ram'], quota.get('ram').limit) self.assertEqual(step.action.initial['injected_files'], - quota.injected_files) + quota.get('injected_files').limit) self.assertEqual(step.action.initial['name'], project.name) self.assertEqual(step.action.initial['description'], project.description) @@ -434,13 +429,14 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): '']) @test.create_stubs({api: ('tenant_get', - 'tenant_quota_get', 'tenant_update', - 'tenant_quota_update', 'get_default_role', 'roles_for_user', 'remove_tenant_user_role', 'add_tenant_user_role'), + api.nova: ('tenant_quota_update',), + api.cinder: ('tenant_quota_update',), + quotas: ('get_tenant_quota_data',), api.keystone: ('user_list', 'role_list',)}) def test_update_project_save(self): @@ -453,8 +449,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): # get/init api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \ .AndReturn(project) - api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \ - .AndReturn(quota) + quotas.get_tenant_quota_data(IsA(http.HttpRequest)).AndReturn(quota) api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users) @@ -524,9 +519,13 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): user_id='3', role_id='1') - api.tenant_quota_update(IsA(http.HttpRequest), + api.nova.tenant_quota_update(IsA(http.HttpRequest), project.id, **updated_quota) + api.cinder.tenant_quota_update(IsA(http.HttpRequest), + project.id, + volumes=updated_quota['volumes'], + gigabytes=updated_quota['gigabytes']) self.mox.ReplayAll() # submit form data @@ -559,13 +558,13 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_stubs({api: ('tenant_get', - 'tenant_quota_get', 'tenant_update', - 'tenant_quota_update', 'get_default_role', 'roles_for_user', 'remove_tenant_user', 'add_tenant_user_role'), + quotas: ('get_tenant_quota_data',), + api.nova: ('tenant_quota_update',), api.keystone: ('user_list', 'role_list',)}) def test_update_project_tenant_update_error(self): @@ -578,8 +577,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): # get/init api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \ .AndReturn(project) - api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \ - .AndReturn(quota) + quotas.get_tenant_quota_data(IsA(http.HttpRequest)).AndReturn(quota) api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users) @@ -631,13 +629,13 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_stubs({api: ('tenant_get', - 'tenant_quota_get', 'tenant_update', - 'tenant_quota_update', 'get_default_role', 'roles_for_user', 'remove_tenant_user_role', 'add_tenant_user_role'), + quotas: ('get_tenant_quota_data',), + api.nova: ('tenant_quota_update',), api.keystone: ('user_list', 'role_list',)}) def test_update_project_quota_update_error(self): @@ -650,8 +648,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): # get/init api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \ .AndReturn(project) - api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \ - .AndReturn(quota) + quotas.get_tenant_quota_data(IsA(http.HttpRequest)).AndReturn(quota) api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users) @@ -708,7 +705,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): user_id='3', role_id='2') - api.tenant_quota_update(IsA(http.HttpRequest), + api.nova.tenant_quota_update(IsA(http.HttpRequest), project.id, **updated_quota).AndRaise(self.exceptions.nova) @@ -730,12 +727,12 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_stubs({api: ('tenant_get', - 'tenant_quota_get', 'tenant_update', 'get_default_role', 'roles_for_user', 'remove_tenant_user_role', 'add_tenant_user_role'), + quotas: ('get_tenant_quota_data',), api.keystone: ('user_list', 'role_list',)}) def test_update_project_member_update_error(self): @@ -748,8 +745,7 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests): # get/init api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \ .AndReturn(project) - api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \ - .AndReturn(quota) + quotas.get_tenant_quota_data(IsA(http.HttpRequest)).AndReturn(quota) api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role) api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users) diff --git a/openstack_dashboard/dashboards/admin/projects/views.py b/openstack_dashboard/dashboards/admin/projects/views.py index 409ac3ca2..1323ecf8e 100644 --- a/openstack_dashboard/dashboards/admin/projects/views.py +++ b/openstack_dashboard/dashboards/admin/projects/views.py @@ -29,6 +29,7 @@ from horizon import workflows from openstack_dashboard import api from openstack_dashboard import usage +from openstack_dashboard.usage import quotas from openstack_dashboard.dashboards.admin.users.views import CreateView from .forms import CreateUser from .tables import TenantsTable, TenantUsersTable, AddUsersTable @@ -145,10 +146,9 @@ class CreateProjectView(workflows.WorkflowView): # get initial quota defaults try: - quota_defaults = api.tenant_quota_defaults(self.request, - self.request.user.tenant_id) + quota_defaults = quotas.get_default_quota_data(self.request) for field in QUOTA_FIELDS: - initial[field] = getattr(quota_defaults, field, None) + initial[field] = quota_defaults.get(field).limit except: error_msg = _('Unable to retrieve default quota values.') @@ -174,9 +174,9 @@ class UpdateProjectView(workflows.WorkflowView): initial[field] = getattr(project_info, field, None) # get initial project quota - quota_data = api.tenant_quota_get(self.request, project_id) + quota_data = quotas.get_tenant_quota_data(self.request) for field in QUOTA_FIELDS: - initial[field] = getattr(quota_data, field, None) + initial[field] = quota_data.get(field).limit except: exceptions.handle(self.request, _('Unable to retrieve project details.'), diff --git a/openstack_dashboard/dashboards/admin/projects/workflows.py b/openstack_dashboard/dashboards/admin/projects/workflows.py index 3b98ad4d6..677c0cd32 100644 --- a/openstack_dashboard/dashboards/admin/projects/workflows.py +++ b/openstack_dashboard/dashboards/admin/projects/workflows.py @@ -28,6 +28,8 @@ from horizon import forms from horizon import messages from openstack_dashboard import api +from openstack_dashboard.api import cinder, nova +from openstack_dashboard.api.base import is_service_enabled INDEX_URL = "horizon:admin:projects:index" @@ -361,17 +363,25 @@ class UpdateProject(workflows.Workflow): # update the project quota ifcb = data['injected_file_content_bytes'] try: - api.tenant_quota_update(request, - project_id, - metadata_items=data['metadata_items'], - injected_file_content_bytes=ifcb, - volumes=data['volumes'], - gigabytes=data['gigabytes'], - ram=data['ram'], - floating_ips=data['floating_ips'], - instances=data['instances'], - injected_files=data['injected_files'], - cores=data['cores']) + # TODO(gabriel): Once nova-volume is fully deprecated the + # "volumes" and "gigabytes" quotas should no longer be sent to + # the nova API to be updated anymore. + nova.tenant_quota_update(request, + project_id, + metadata_items=data['metadata_items'], + injected_file_content_bytes=ifcb, + volumes=data['volumes'], + gigabytes=data['gigabytes'], + ram=data['ram'], + floating_ips=data['floating_ips'], + instances=data['instances'], + injected_files=data['injected_files'], + cores=data['cores']) + if is_service_enabled(request, 'volume'): + cinder.tenant_quota_update(request, + project_id, + volumes=data['volumes'], + gigabytes=data['gigabytes']) return True except: exceptions.handle(request, _('Modified project information and ' diff --git a/openstack_dashboard/dashboards/admin/quotas/views.py b/openstack_dashboard/dashboards/admin/quotas/views.py index 68d615759..0d70d95b0 100644 --- a/openstack_dashboard/dashboards/admin/quotas/views.py +++ b/openstack_dashboard/dashboards/admin/quotas/views.py @@ -38,7 +38,7 @@ class IndexView(tables.DataTableView): def get_data(self): try: - quota_set = api.tenant_quota_defaults(self.request, + quota_set = api.default_quota_get(self.request, self.request.user.tenant_id) data = quota_set.items except: diff --git a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/views.py b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/views.py index 350110b52..3daaec49f 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/views.py +++ b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/views.py @@ -31,6 +31,7 @@ from horizon import forms from horizon import workflows from openstack_dashboard import api +from openstack_dashboard.usage import quotas from .forms import FloatingIpAllocate from .workflows import IPAssociationWorkflow @@ -51,7 +52,7 @@ class AllocateView(forms.ModalFormView): def get_context_data(self, **kwargs): context = super(AllocateView, self).get_context_data(**kwargs) try: - context['usages'] = api.tenant_quota_usages(self.request) + context['usages'] = quotas.tenant_quota_usages(self.request) except: exceptions.handle(self.request) return context diff --git a/openstack_dashboard/dashboards/project/images_and_snapshots/volume_snapshots/tables.py b/openstack_dashboard/dashboards/project/images_and_snapshots/volume_snapshots/tables.py index 1ef6e02d3..fbb4474f0 100644 --- a/openstack_dashboard/dashboards/project/images_and_snapshots/volume_snapshots/tables.py +++ b/openstack_dashboard/dashboards/project/images_and_snapshots/volume_snapshots/tables.py @@ -24,6 +24,7 @@ from django.utils.translation import ugettext_lazy as _ from horizon import tables from openstack_dashboard import api +from openstack_dashboard.api import cinder from ...volumes import tables as volume_tables @@ -57,7 +58,7 @@ class UpdateRow(tables.Row): ajax = True def get_data(self, request, snapshot_id): - snapshot = api.nova.volume_snapshot_get(request, snapshot_id) + snapshot = cinder.volume_snapshot_get(request, snapshot_id) return snapshot diff --git a/openstack_dashboard/dashboards/project/images_and_snapshots/volume_snapshots/tabs.py b/openstack_dashboard/dashboards/project/images_and_snapshots/volume_snapshots/tabs.py index cd4c2222e..f756c55dc 100644 --- a/openstack_dashboard/dashboards/project/images_and_snapshots/volume_snapshots/tabs.py +++ b/openstack_dashboard/dashboards/project/images_and_snapshots/volume_snapshots/tabs.py @@ -20,7 +20,7 @@ from django.utils.translation import ugettext_lazy as _ from horizon import exceptions from horizon import tabs -from openstack_dashboard import api +from openstack_dashboard.api import cinder class OverviewTab(tabs.Tab): @@ -32,8 +32,8 @@ class OverviewTab(tabs.Tab): def get_context_data(self, request): snapshot_id = self.tab_group.kwargs['snapshot_id'] try: - snapshot = api.nova.volume_snapshot_get(request, snapshot_id) - volume = api.nova.volume_get(request, snapshot.volume_id) + snapshot = cinder.volume_snapshot_get(request, snapshot_id) + volume = cinder.volume_get(request, snapshot.volume_id) volume.display_name = None except: redirect = reverse('horizon:project:images_and_snapshots:index') diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index 43ef81fa1..d7321ce61 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -26,7 +26,9 @@ from django.utils.datastructures import SortedDict from mox import IsA, IgnoreArg from openstack_dashboard import api +from openstack_dashboard.api import cinder from openstack_dashboard.test import helpers as test +from openstack_dashboard.usage import quotas from .tabs import InstanceDetailTabs from .workflows import LaunchInstance @@ -324,7 +326,7 @@ class InstanceTests(test.TestCase): self.assertRedirectsNoFollow(res, INDEX_URL) @test.create_stubs({api: ("server_get", - "volume_instance_list", + "instance_volumes_list", "flavor_get", "server_security_groups")}) def test_instance_details_volumes(self): @@ -332,8 +334,8 @@ class InstanceTests(test.TestCase): volumes = [self.volumes.list()[1]] api.server_get(IsA(http.HttpRequest), server.id).AndReturn(server) - api.volume_instance_list(IsA(http.HttpRequest), - server.id).AndReturn(volumes) + api.instance_volumes_list(IsA(http.HttpRequest), + server.id).AndReturn(volumes) api.flavor_get(IsA(http.HttpRequest), server.flavor['id']).AndReturn(self.flavors.first()) api.server_security_groups(IsA(http.HttpRequest), @@ -348,7 +350,7 @@ class InstanceTests(test.TestCase): self.assertItemsEqual(res.context['instance'].volumes, volumes) @test.create_stubs({api: ("server_get", - "volume_instance_list", + "instance_volumes_list", "flavor_get", "server_security_groups")}) def test_instance_details_volume_sorting(self): @@ -356,8 +358,8 @@ class InstanceTests(test.TestCase): volumes = self.volumes.list()[1:3] api.server_get(IsA(http.HttpRequest), server.id).AndReturn(server) - api.volume_instance_list(IsA(http.HttpRequest), - server.id).AndReturn(volumes) + api.instance_volumes_list(IsA(http.HttpRequest), + server.id).AndReturn(volumes) api.flavor_get(IsA(http.HttpRequest), server.flavor['id']).AndReturn(self.flavors.first()) api.server_security_groups(IsA(http.HttpRequest), @@ -376,15 +378,15 @@ class InstanceTests(test.TestCase): "/dev/hdk") @test.create_stubs({api: ("server_get", - "volume_instance_list", + "instance_volumes_list", "flavor_get", "server_security_groups",)}) def test_instance_details_metadata(self): server = self.servers.first() api.server_get(IsA(http.HttpRequest), server.id).AndReturn(server) - api.volume_instance_list(IsA(http.HttpRequest), - server.id).AndReturn([]) + api.instance_volumes_list(IsA(http.HttpRequest), + server.id).AndReturn([]) api.flavor_get(IsA(http.HttpRequest), server.flavor['id']).AndReturn(self.flavors.first()) api.server_security_groups(IsA(http.HttpRequest), @@ -582,30 +584,30 @@ class InstanceTests(test.TestCase): self.assertRedirectsNoFollow(res, INDEX_URL) - @test.create_stubs({api.nova: ('tenant_quota_usages', - 'flavor_list', + @test.create_stubs({api.nova: ('flavor_list', 'keypair_list', - 'security_group_list', - 'volume_snapshot_list', - 'volume_list',), + 'security_group_list',), + cinder: ('volume_snapshot_list', + 'volume_list',), + quotas: ('tenant_quota_usages',), api.quantum: ('network_list',), api.glance: ('image_list_detailed',)}) def test_launch_instance_get(self): quota_usages = self.quota_usages.first() image = self.images.first() - api.nova.volume_list(IsA(http.HttpRequest)) \ + cinder.volume_list(IsA(http.HttpRequest)) \ + .AndReturn(self.volumes.list()) + cinder.volume_snapshot_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) - api.nova.volume_snapshot_list(IsA(http.HttpRequest)) \ - .AndReturn(self.volumes.list()) api.glance.image_list_detailed(IsA(http.HttpRequest), filters={'is_public': True, 'status': 'active'}) \ - .AndReturn([self.images.list(), False]) + .AndReturn([self.images.list(), False]) api.glance.image_list_detailed(IsA(http.HttpRequest), filters={'property-owner_id': self.tenant.id, 'status': 'active'}) \ - .AndReturn([[], False]) + .AndReturn([[], False]) api.quantum.network_list(IsA(http.HttpRequest), tenant_id=self.tenant.id, shared=False) \ @@ -613,7 +615,7 @@ class InstanceTests(test.TestCase): api.quantum.network_list(IsA(http.HttpRequest), shared=True) \ .AndReturn(self.networks.list()[1:]) - api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \ + quotas.tenant_quota_usages(IsA(http.HttpRequest)) \ .AndReturn(quota_usages) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) @@ -646,13 +648,13 @@ class InstanceTests(test.TestCase): @test.create_stubs({api.glance: ('image_list_detailed',), api.quantum: ('network_list',), + quotas: ('tenant_quota_usages',), api.nova: ('flavor_list', 'keypair_list', 'security_group_list', - 'volume_list', - 'volume_snapshot_list', - 'tenant_quota_usages', - 'server_create',)}) + 'server_create',), + cinder: ('volume_list', + 'volume_snapshot_list',)}) def test_launch_instance_post(self): flavor = self.flavors.first() image = self.images.first() @@ -687,9 +689,9 @@ class InstanceTests(test.TestCase): api.quantum.network_list(IsA(http.HttpRequest), shared=True) \ .AndReturn(self.networks.list()[1:]) - api.nova.volume_list(IsA(http.HttpRequest)) \ + cinder.volume_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) - api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) + cinder.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) api.nova.server_create(IsA(http.HttpRequest), server.name, image.id, @@ -725,12 +727,12 @@ class InstanceTests(test.TestCase): @test.create_stubs({api.glance: ('image_list_detailed',), api.quantum: ('network_list',), - api.nova: ('flavor_list', - 'keypair_list', - 'security_group_list', - 'volume_list', - 'tenant_quota_usages', - 'volume_snapshot_list',)}) + quotas: ('tenant_quota_usages',), + api.nova: ('flavor_list', + 'keypair_list', + 'security_group_list',), + cinder: ('volume_list', + 'volume_snapshot_list',)}) def test_launch_instance_post_no_images_available(self): flavor = self.flavors.first() keypair = self.keypairs.first() @@ -743,7 +745,7 @@ class InstanceTests(test.TestCase): api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) - api.nova.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn({}) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn({}) api.glance.image_list_detailed(IsA(http.HttpRequest), filters={'is_public': True, 'status': 'active'}) \ @@ -765,9 +767,9 @@ class InstanceTests(test.TestCase): .AndReturn(self.keypairs.list()) api.nova.security_group_list(IsA(http.HttpRequest)) \ .AndReturn(self.security_groups.list()) - api.nova.volume_list(IsA(http.HttpRequest)) \ + cinder.volume_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) - api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) + cinder.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) self.mox.ReplayAll() @@ -795,16 +797,16 @@ class InstanceTests(test.TestCase): @test.create_stubs({api.glance: ('image_list_detailed',), api.quantum: ('network_list',), - api.nova: ('tenant_quota_usages', - 'flavor_list', + quotas: ('tenant_quota_usages',), + cinder: ('volume_list', + 'volume_snapshot_list',), + api.nova: ('flavor_list', 'keypair_list', - 'volume_list', - 'security_group_list', - 'volume_snapshot_list',)}) + 'security_group_list',)}) def test_launch_flavorlist_error(self): - api.nova.volume_list(IsA(http.HttpRequest)) \ + cinder.volume_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) - api.nova.volume_snapshot_list(IsA(http.HttpRequest)) \ + cinder.volume_snapshot_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) api.glance.image_list_detailed(IsA(http.HttpRequest), filters={'is_public': True, @@ -821,7 +823,7 @@ class InstanceTests(test.TestCase): api.quantum.network_list(IsA(http.HttpRequest), shared=True) \ .AndReturn(self.networks.list()[1:]) - api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \ + quotas.tenant_quota_usages(IsA(http.HttpRequest)) \ .AndReturn(self.quota_usages.first()) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndRaise(self.exceptions.nova) @@ -845,9 +847,9 @@ class InstanceTests(test.TestCase): api.nova: ('flavor_list', 'keypair_list', 'security_group_list', - 'volume_list', - 'server_create', - 'volume_snapshot_list',)}) + 'server_create',), + cinder: ('volume_list', + 'volume_snapshot_list',)}) def test_launch_form_keystone_exception(self): flavor = self.flavors.first() image = self.images.first() @@ -857,8 +859,8 @@ class InstanceTests(test.TestCase): customization_script = 'userData' nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}] - api.nova.volume_snapshot_list(IsA(http.HttpRequest)) \ - .AndReturn(self.volumes.list()) + cinder.volume_snapshot_list(IsA(http.HttpRequest)) \ + .AndReturn(self.volumes.list()) api.nova.flavor_list(IgnoreArg()).AndReturn(self.flavors.list()) api.nova.keypair_list(IgnoreArg()).AndReturn(self.keypairs.list()) api.nova.security_group_list(IsA(http.HttpRequest)) \ @@ -878,7 +880,7 @@ class InstanceTests(test.TestCase): api.quantum.network_list(IsA(http.HttpRequest), shared=True) \ .AndReturn(self.networks.list()[1:]) - api.nova.volume_list(IgnoreArg()).AndReturn(self.volumes.list()) + cinder.volume_list(IgnoreArg()).AndReturn(self.volumes.list()) api.nova.server_create(IsA(http.HttpRequest), server.name, image.id, @@ -912,12 +914,12 @@ class InstanceTests(test.TestCase): @test.create_stubs({api.glance: ('image_list_detailed',), api.quantum: ('network_list',), + quotas: ('tenant_quota_usages',), api.nova: ('flavor_list', 'keypair_list', - 'security_group_list', - 'volume_list', - 'tenant_quota_usages', - 'volume_snapshot_list',)}) + 'security_group_list',), + cinder: ('volume_list', + 'volume_snapshot_list',)}) def test_launch_form_instance_count_error(self): flavor = self.flavors.first() image = self.images.first() @@ -950,13 +952,13 @@ class InstanceTests(test.TestCase): api.quantum.network_list(IsA(http.HttpRequest), shared=True) \ .AndReturn(self.networks.list()[1:]) - api.nova.volume_list(IsA(http.HttpRequest)) \ + cinder.volume_list(IsA(http.HttpRequest)) \ .AndReturn(self.volumes.list()) - api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) + cinder.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) - api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \ + quotas.tenant_quota_usages(IsA(http.HttpRequest)) \ .AndReturn(self.quota_usages.first()) self.mox.ReplayAll() diff --git a/openstack_dashboard/dashboards/project/instances/views.py b/openstack_dashboard/dashboards/project/instances/views.py index 9cea06ecf..c4df849cb 100644 --- a/openstack_dashboard/dashboards/project/instances/views.py +++ b/openstack_dashboard/dashboards/project/instances/views.py @@ -165,8 +165,8 @@ class DetailView(tabs.TabView): try: instance_id = self.kwargs['instance_id'] instance = api.server_get(self.request, instance_id) - instance.volumes = api.volume_instance_list(self.request, - instance_id) + instance.volumes = api.instance_volumes_list(self.request, + instance_id) # Sort by device name instance.volumes.sort(key=lambda vol: vol.device) instance.full_flavor = api.flavor_get(self.request, diff --git a/openstack_dashboard/dashboards/project/instances/workflows.py b/openstack_dashboard/dashboards/project/instances/workflows.py index 53b382319..d3a327b01 100644 --- a/openstack_dashboard/dashboards/project/instances/workflows.py +++ b/openstack_dashboard/dashboards/project/instances/workflows.py @@ -29,6 +29,8 @@ from horizon import forms from horizon import workflows from openstack_dashboard import api +from openstack_dashboard.api import cinder +from openstack_dashboard.usage import quotas LOG = logging.getLogger(__name__) @@ -116,7 +118,7 @@ class VolumeOptionsAction(workflows.Action): def populate_volume_id_choices(self, request, context): volume_options = [("", _("Select Volume"))] try: - volumes = [v for v in api.nova.volume_list(self.request) + volumes = [v for v in cinder.volume_list(self.request) if v.status == api.VOLUME_STATE_AVAILABLE] volume_options.extend([self._get_volume_display_name(vol) for vol in volumes]) @@ -128,7 +130,7 @@ class VolumeOptionsAction(workflows.Action): def populate_volume_snapshot_id_choices(self, request, context): volume_options = [("", _("Select Volume Snapshot"))] try: - snapshots = api.nova.volume_snapshot_list(self.request) + snapshots = cinder.volume_snapshot_list(self.request) snapshots = [s for s in snapshots if s.status == api.VOLUME_STATE_AVAILABLE] volume_options.extend([self._get_volume_display_name(snap) @@ -294,7 +296,7 @@ class SetInstanceDetailsAction(workflows.Action): def get_help_text(self): extra = {} try: - extra['usages'] = api.nova.tenant_quota_usages(self.request) + extra['usages'] = quotas.tenant_quota_usages(self.request) extra['usages_json'] = json.dumps(extra['usages']) flavors = json.dumps([f._info for f in api.nova.flavor_list(self.request)]) diff --git a/openstack_dashboard/dashboards/project/overview/tests.py b/openstack_dashboard/dashboards/project/overview/tests.py index dac718ff5..a16873f51 100644 --- a/openstack_dashboard/dashboards/project/overview/tests.py +++ b/openstack_dashboard/dashboards/project/overview/tests.py @@ -27,8 +27,9 @@ from django.utils import timezone from mox import IsA, Func from openstack_dashboard import api -from openstack_dashboard.test import helpers as test from openstack_dashboard import usage +from openstack_dashboard.test import helpers as test +from openstack_dashboard.usage import quotas INDEX_URL = reverse('horizon:project:overview:index') @@ -37,15 +38,15 @@ INDEX_URL = reverse('horizon:project:overview:index') class UsageViewTests(test.TestCase): def test_usage(self): now = timezone.now() - usage_obj = api.nova.Usage(self.usages.first()) - quotas = self.quota_usages.first() + usage_obj = api.nova.NovaUsage(self.usages.first()) + quota_data = self.quota_usages.first() self.mox.StubOutWithMock(api, 'usage_get') - self.mox.StubOutWithMock(api.nova, 'tenant_quota_usages') + self.mox.StubOutWithMock(quotas, 'tenant_quota_usages') api.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) - api.nova.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quotas) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data) self.mox.ReplayAll() res = self.client.get(reverse('horizon:project:overview:index')) @@ -69,17 +70,17 @@ class UsageViewTests(test.TestCase): def test_usage_csv(self): now = timezone.now() - usage_obj = api.nova.Usage(self.usages.first()) - quotas = self.quota_usages.first() + usage_obj = api.nova.NovaUsage(self.usages.first()) + quota_data = self.quota_usages.first() self.mox.StubOutWithMock(api, 'usage_get') - self.mox.StubOutWithMock(api.nova, 'tenant_quota_usages') + self.mox.StubOutWithMock(quotas, 'tenant_quota_usages') timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) api.usage_get(IsA(http.HttpRequest), self.tenant.id, timestamp, Func(usage.almost_now)) \ .AndReturn(usage_obj) - api.nova.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quotas) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data) self.mox.ReplayAll() res = self.client.get(reverse('horizon:project:overview:index') + @@ -89,16 +90,16 @@ class UsageViewTests(test.TestCase): def test_usage_exception_usage(self): now = timezone.now() - quotas = self.quota_usages.first() + quota_data = self.quota_usages.first() self.mox.StubOutWithMock(api, 'usage_get') - self.mox.StubOutWithMock(api.nova, 'tenant_quota_usages') + self.mox.StubOutWithMock(quotas, 'tenant_quota_usages') timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) api.usage_get(IsA(http.HttpRequest), self.tenant.id, timestamp, Func(usage.almost_now)) \ .AndRaise(self.exceptions.nova) - api.nova.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quotas) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data) self.mox.ReplayAll() res = self.client.get(reverse('horizon:project:overview:index')) @@ -107,16 +108,16 @@ class UsageViewTests(test.TestCase): def test_usage_exception_quota(self): now = timezone.now() - usage_obj = api.nova.Usage(self.usages.first()) + usage_obj = api.nova.NovaUsage(self.usages.first()) self.mox.StubOutWithMock(api, 'usage_get') - self.mox.StubOutWithMock(api.nova, 'tenant_quota_usages') + self.mox.StubOutWithMock(quotas, 'tenant_quota_usages') timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) api.usage_get(IsA(http.HttpRequest), self.tenant.id, timestamp, Func(usage.almost_now)) \ .AndReturn(usage_obj) - api.nova.tenant_quota_usages(IsA(http.HttpRequest))\ + quotas.tenant_quota_usages(IsA(http.HttpRequest))\ .AndRaise(self.exceptions.nova) self.mox.ReplayAll() @@ -126,17 +127,17 @@ class UsageViewTests(test.TestCase): def test_usage_default_tenant(self): now = timezone.now() - usage_obj = api.nova.Usage(self.usages.first()) - quotas = self.quota_usages.first() + usage_obj = api.nova.NovaUsage(self.usages.first()) + quota_data = self.quota_usages.first() self.mox.StubOutWithMock(api, 'usage_get') - self.mox.StubOutWithMock(api.nova, 'tenant_quota_usages') + self.mox.StubOutWithMock(quotas, 'tenant_quota_usages') timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) api.usage_get(IsA(http.HttpRequest), self.tenant.id, timestamp, Func(usage.almost_now)) \ .AndReturn(usage_obj) - api.nova.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quotas) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data) self.mox.ReplayAll() res = self.client.get(reverse('horizon:project:overview:index')) diff --git a/openstack_dashboard/dashboards/project/volumes/forms.py b/openstack_dashboard/dashboards/project/volumes/forms.py index 793f50311..c9d0ffd36 100644 --- a/openstack_dashboard/dashboards/project/volumes/forms.py +++ b/openstack_dashboard/dashboards/project/volumes/forms.py @@ -19,6 +19,8 @@ from horizon.utils.fields import SelectWidget from horizon.utils.memoized import memoized from openstack_dashboard import api +from openstack_dashboard.api import cinder +from openstack_dashboard.usage import quotas from ..instances.tables import ACTIVE_STATES @@ -71,7 +73,7 @@ class CreateForm(forms.SelfHandlingForm): # error message when the quota is exceeded when trying to create # a volume, so we need to check for that scenario here before we # send it off to try and create. - usages = api.tenant_quota_usages(request) + usages = quotas.tenant_quota_usages(request) snapshot_id = None if (data.get("snapshot_source", None)): @@ -116,7 +118,7 @@ class CreateForm(forms.SelfHandlingForm): @memoized def get_snapshot(self, request, id): - return api.nova.volume_snapshot_get(request, id) + return cinder.volume_snapshot_get(request, id) class AttachForm(forms.SelfHandlingForm): @@ -170,10 +172,10 @@ class AttachForm(forms.SelfHandlingForm): # it, so let's slice that off... instance_name = instance_name.rsplit(" (")[0] try: - vol = api.volume_attach(request, - data['volume_id'], - data['instance'], - data.get('device', '')) + vol = api.instance_volume_attach(request, + data['volume_id'], + data['instance'], + data.get('device', '')) vol_name = api.volume_get(request, data['volume_id']).display_name message = _('Attaching volume %(vol)s to instance ' diff --git a/openstack_dashboard/dashboards/project/volumes/tables.py b/openstack_dashboard/dashboards/project/volumes/tables.py index 4be2ba88f..43349907a 100644 --- a/openstack_dashboard/dashboards/project/volumes/tables.py +++ b/openstack_dashboard/dashboards/project/volumes/tables.py @@ -185,7 +185,9 @@ class DetachVolume(tables.BatchAction): def action(self, request, obj_id): attachment = self.table.get_object_by_id(obj_id) - api.volume_detach(request, attachment.get('server_id', None), obj_id) + api.instance_volume_detach(request, + attachment.get('server_id', None), + obj_id) def get_success_url(self, request): return reverse('horizon:project:volumes:index') diff --git a/openstack_dashboard/dashboards/project/volumes/tabs.py b/openstack_dashboard/dashboards/project/volumes/tabs.py index 0d00ed899..977190eca 100644 --- a/openstack_dashboard/dashboards/project/volumes/tabs.py +++ b/openstack_dashboard/dashboards/project/volumes/tabs.py @@ -20,7 +20,7 @@ from django.utils.translation import ugettext_lazy as _ from horizon import exceptions from horizon import tabs -from openstack_dashboard import api +from openstack_dashboard.api import cinder, nova class OverviewTab(tabs.Tab): @@ -32,10 +32,9 @@ class OverviewTab(tabs.Tab): def get_context_data(self, request): volume_id = self.tab_group.kwargs['volume_id'] try: - volume = api.nova.volume_get(request, volume_id) + volume = cinder.volume_get(request, volume_id) for att in volume.attachments: - att['instance'] = api.nova.server_get(request, - att['server_id']) + att['instance'] = nova.server_get(request, att['server_id']) except: redirect = reverse('horizon:project:volumes:index') exceptions.handle(self.request, diff --git a/openstack_dashboard/dashboards/project/volumes/tests.py b/openstack_dashboard/dashboards/project/volumes/tests.py index 3c28442fa..f1959e8c3 100644 --- a/openstack_dashboard/dashboards/project/volumes/tests.py +++ b/openstack_dashboard/dashboards/project/volumes/tests.py @@ -26,12 +26,15 @@ from django.forms import widgets from mox import IsA from openstack_dashboard import api +from openstack_dashboard.api import cinder from openstack_dashboard.test import helpers as test +from openstack_dashboard.usage import quotas class VolumeViewTests(test.TestCase): - @test.create_stubs({api: ('tenant_quota_usages', 'volume_create', - 'volume_snapshot_list')}) + @test.create_stubs({api: ('volume_create', + 'volume_snapshot_list'), + quotas: ('tenant_quota_usages',)}) def test_create_volume(self): volume = self.volumes.first() usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}} @@ -40,7 +43,7 @@ class VolumeViewTests(test.TestCase): 'method': u'CreateForm', 'size': 50, 'snapshot_source': ''} - api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) api.volume_snapshot_list(IsA(http.HttpRequest)).\ AndReturn(self.volume_snapshots.list()) api.volume_create(IsA(http.HttpRequest), @@ -57,9 +60,10 @@ class VolumeViewTests(test.TestCase): redirect_url = reverse('horizon:project:volumes:index') self.assertRedirectsNoFollow(res, redirect_url) - @test.create_stubs({api: ('tenant_quota_usages', 'volume_create', + @test.create_stubs({api: ('volume_create', 'volume_snapshot_list'), - api.nova: ('volume_snapshot_get',)}) + cinder: ('volume_snapshot_get',), + quotas: ('tenant_quota_usages',)}) def test_create_volume_from_snapshot(self): volume = self.volumes.first() usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}} @@ -70,9 +74,9 @@ class VolumeViewTests(test.TestCase): 'size': 50, 'snapshot_source': snapshot.id} # first call- with url param - api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) - api.nova.volume_snapshot_get(IsA(http.HttpRequest), - str(snapshot.id)).AndReturn(snapshot) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + cinder.volume_snapshot_get(IsA(http.HttpRequest), + str(snapshot.id)).AndReturn(snapshot) api.volume_create(IsA(http.HttpRequest), formData['size'], formData['name'], @@ -80,11 +84,11 @@ class VolumeViewTests(test.TestCase): snapshot_id=snapshot.id).\ AndReturn(volume) # second call- with dropdown - api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) api.volume_snapshot_list(IsA(http.HttpRequest)).\ AndReturn(self.volume_snapshots.list()) - api.nova.volume_snapshot_get(IsA(http.HttpRequest), - str(snapshot.id)).AndReturn(snapshot) + cinder.volume_snapshot_get(IsA(http.HttpRequest), + str(snapshot.id)).AndReturn(snapshot) api.volume_create(IsA(http.HttpRequest), formData['size'], formData['name'], @@ -110,8 +114,8 @@ class VolumeViewTests(test.TestCase): redirect_url = reverse('horizon:project:volumes:index') self.assertRedirectsNoFollow(res, redirect_url) - @test.create_stubs({api: ('tenant_quota_usages',), - api.nova: ('volume_snapshot_get',)}) + @test.create_stubs({cinder: ('volume_snapshot_get',), + quotas: ('tenant_quota_usages',)}) def test_create_volume_from_snapshot_invalid_size(self): usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}} snapshot = self.volume_snapshots.first() @@ -120,10 +124,10 @@ class VolumeViewTests(test.TestCase): 'method': u'CreateForm', 'size': 20, 'snapshot_source': snapshot.id} - api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) - api.nova.volume_snapshot_get(IsA(http.HttpRequest), - str(snapshot.id)).AndReturn(snapshot) - api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + cinder.volume_snapshot_get(IsA(http.HttpRequest), + str(snapshot.id)).AndReturn(snapshot) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) self.mox.ReplayAll() @@ -136,7 +140,8 @@ class VolumeViewTests(test.TestCase): "The volume size cannot be less than the " "snapshot size (40GB)") - @test.create_stubs({api: ('tenant_quota_usages', 'volume_snapshot_list')}) + @test.create_stubs({api: ('volume_snapshot_list',), + quotas: ('tenant_quota_usages',)}) def test_create_volume_gb_used_over_alloted_quota(self): usage = {'gigabytes': {'available': 100, 'used': 20}} formData = {'name': u'This Volume Is Huge!', @@ -144,10 +149,10 @@ class VolumeViewTests(test.TestCase): 'method': u'CreateForm', 'size': 5000} - api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) api.volume_snapshot_list(IsA(http.HttpRequest)).\ AndReturn(self.volume_snapshots.list()) - api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) self.mox.ReplayAll() @@ -158,7 +163,8 @@ class VolumeViewTests(test.TestCase): ' have 100GB of your quota available.'] self.assertEqual(res.context['form'].errors['__all__'], expected_error) - @test.create_stubs({api: ('tenant_quota_usages', 'volume_snapshot_list')}) + @test.create_stubs({api: ('volume_snapshot_list',), + quotas: ('tenant_quota_usages',)}) def test_create_volume_number_over_alloted_quota(self): usage = {'gigabytes': {'available': 100, 'used': 20}, 'volumes': {'available': 0}} @@ -167,10 +173,10 @@ class VolumeViewTests(test.TestCase): 'method': u'CreateForm', 'size': 10} - api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) api.volume_snapshot_list(IsA(http.HttpRequest)).\ AndReturn(self.volume_snapshots.list()) - api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) self.mox.ReplayAll() @@ -297,14 +303,15 @@ class VolumeViewTests(test.TestCase): server.id) self.assertEqual(res.status_code, 200) - @test.create_stubs({api.nova: ('volume_get', 'server_get',)}) + @test.create_stubs({cinder: ('volume_get',), + api.nova: ('server_get',)}) def test_detail_view(self): volume = self.volumes.first() server = self.servers.first() volume.attachments = [{"server_id": server.id}] - api.nova.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume) + cinder.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume) api.nova.server_get(IsA(http.HttpRequest), server.id).AndReturn(server) self.mox.ReplayAll() diff --git a/openstack_dashboard/dashboards/project/volumes/views.py b/openstack_dashboard/dashboards/project/volumes/views.py index 6de6ff9e6..a9516cc06 100644 --- a/openstack_dashboard/dashboards/project/volumes/views.py +++ b/openstack_dashboard/dashboards/project/volumes/views.py @@ -30,6 +30,7 @@ from horizon import tables from horizon import tabs from openstack_dashboard import api +from openstack_dashboard.usage import quotas from .forms import CreateForm, AttachForm, CreateSnapshotForm from .tables import AttachmentsTable, VolumesTable from .tabs import VolumeDetailTabs @@ -93,7 +94,7 @@ class CreateView(forms.ModalFormView): def get_context_data(self, **kwargs): context = super(CreateView, self).get_context_data(**kwargs) try: - context['usages'] = api.tenant_quota_usages(self.request) + context['usages'] = quotas.tenant_quota_usages(self.request) except: exceptions.handle(self.request) return context diff --git a/openstack_dashboard/test/api_tests/nova_tests.py b/openstack_dashboard/test/api_tests/nova_tests.py index d3f3ed476..f247de6d4 100644 --- a/openstack_dashboard/test/api_tests/nova_tests.py +++ b/openstack_dashboard/test/api_tests/nova_tests.py @@ -96,7 +96,7 @@ class ComputeApiTests(test.APITestCase): self.mox.ReplayAll() ret_val = api.usage_get(self.request, self.tenant.id, 'start', 'end') - self.assertIsInstance(ret_val, api.nova.Usage) + self.assertIsInstance(ret_val, api.nova.NovaUsage) def test_usage_list(self): usages = self.usages.list() @@ -108,7 +108,7 @@ class ComputeApiTests(test.APITestCase): ret_val = api.usage_list(self.request, 'start', 'end') for usage in ret_val: - self.assertIsInstance(usage, api.Usage) + self.assertIsInstance(usage, api.NovaUsage) def test_server_get(self): server = self.servers.first() @@ -156,51 +156,3 @@ class ComputeApiTests(test.APITestCase): server.id, floating_ip.id) self.assertIsInstance(server, api.nova.Server) - - @test.create_stubs({api.nova: ('volume_list', - 'server_list', - 'flavor_list', - 'tenant_floating_ip_list', - 'tenant_quota_get',)}) - def test_tenant_quota_usages(self): - api.nova.flavor_list(IsA(http.HttpRequest)) \ - .AndReturn(self.flavors.list()) - api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ - .AndReturn(self.quotas.first()) - api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ - .AndReturn(self.floating_ips.list()) - api.nova.server_list(IsA(http.HttpRequest)) \ - .AndReturn(self.servers.list()) - api.nova.volume_list(IsA(http.HttpRequest)) \ - .AndReturn(self.volumes.list()) - - self.mox.ReplayAll() - - quota_usages = api.tenant_quota_usages(self.request) - expected_output = {'gigabytes': { - 'used': 80, - 'flavor_fields': [], - 'quota': 1000}, - 'ram': { - 'available': 8976, - 'used': 1024, - 'flavor_fields': ['ram'], - 'quota': 10000}, - 'floating_ips': { - 'used': 2, - 'flavor_fields': [], - 'quota': 1}, - 'instances': { - 'used': 2, - 'flavor_fields': [], - 'quota': 10}, - 'volumes': { - 'used': 3, - 'flavor_fields': [], - 'quota': 1}, - 'cores': { - 'used': 2, - 'flavor_fields': ['vcpus'], - 'quota': 10}} - - self.assertEquals(quota_usages, expected_output) diff --git a/openstack_dashboard/test/test_data/nova_data.py b/openstack_dashboard/test/test_data/nova_data.py index a2876f5bb..8b8f12842 100644 --- a/openstack_dashboard/test/test_data/nova_data.py +++ b/openstack_dashboard/test/test_data/nova_data.py @@ -20,6 +20,8 @@ from novaclient.v1_1 import (flavors, keypairs, servers, volumes, quotas, security_group_rules as rules, security_groups as sec_groups) +from openstack_dashboard.api.base import Quota, QuotaSet as QuotaSetWrapper +from openstack_dashboard.usage.quotas import QuotaUsage from .utils import TestDataContainer @@ -265,21 +267,23 @@ def data(TEST): injected_files='1', cores='10') quota = quotas.QuotaSet(quotas.QuotaSetManager(None), quota_data) - TEST.quotas.add(quota) + TEST.quotas.add(QuotaSetWrapper(quota)) # Quota Usages - TEST.quota_usages.add({'gigabytes': {'available': 1000, - 'used': 0, - 'quota': 1000}, - 'instances': {'available': 10, - 'used': 0, - 'quota': 10}, - 'ram': {'available': 10000, - 'used': 0, - 'quota': 10000}, - 'cores': {'available': 20, - 'used': 0, - 'quota': 20}}) + quota_usage_data = {'gigabytes': {'used': 0, + 'quota': 1000}, + 'instances': {'used': 0, + 'quota': 10}, + 'ram': {'used': 0, + 'quota': 10000}, + 'cores': {'used': 0, + 'quota': 20}} + quota_usage = QuotaUsage() + for k, v in quota_usage_data.items(): + quota_usage.add_quota(Quota(k, v['quota'])) + quota_usage.tally(k, v['used']) + + TEST.quota_usages.add(quota_usage) # Servers vals = {"host": "http://nova.example.com:8774", diff --git a/openstack_dashboard/test/tests/quotas.py b/openstack_dashboard/test/tests/quotas.py new file mode 100644 index 000000000..8590eaecc --- /dev/null +++ b/openstack_dashboard/test/tests/quotas.py @@ -0,0 +1,69 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 Nebula, Inc. +# Copyright (c) 2012 X.commerce, a business unit of eBay Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +from django import http +from mox import IsA + +from openstack_dashboard import api +from openstack_dashboard.api import cinder +from openstack_dashboard.test import helpers as test +from openstack_dashboard.usage import quotas + + +class QuotaTests(test.APITestCase): + @test.create_stubs({api.nova: ('server_list', + 'flavor_list', + 'tenant_floating_ip_list', + 'tenant_quota_get',), + cinder: ('volume_list', 'tenant_quota_get',)}) + def test_tenant_quota_usages(self): + api.nova.flavor_list(IsA(http.HttpRequest)) \ + .AndReturn(self.flavors.list()) + api.nova.tenant_quota_get(IsA(http.HttpRequest), '1') \ + .AndReturn(self.quotas.first()) + api.nova.tenant_floating_ip_list(IsA(http.HttpRequest)) \ + .AndReturn(self.floating_ips.list()) + api.nova.server_list(IsA(http.HttpRequest)) \ + .AndReturn(self.servers.list()) + cinder.volume_list(IsA(http.HttpRequest)) \ + .AndReturn(self.volumes.list()) + cinder.tenant_quota_get(IsA(http.HttpRequest), '1') \ + .AndReturn(self.quotas.first()) + + self.mox.ReplayAll() + + quota_usages = quotas.tenant_quota_usages(self.request) + expected_output = { + 'injected_file_content_bytes': {'quota': 1}, + 'metadata_items': {'quota': 1}, + 'injected_files': {'quota': 1}, + 'gigabytes': {'available': 920, 'used': 80, 'quota': 1000}, + 'ram': {'available': 8976, 'used': 1024, 'quota': 10000}, + 'floating_ips': {'available': 0, 'used': 2, 'quota': 1}, + 'instances': {'available': 8, 'used': 2, 'quota': 10}, + 'volumes': {'available': 0, 'used': 3, 'quota': 1}, + 'cores': {'available': 8, 'used': 2, 'quota': 10} + } + + # Compare internal structure of usages to expected. + self.assertEquals(quota_usages.usages, expected_output) diff --git a/openstack_dashboard/usage/base.py b/openstack_dashboard/usage/base.py index a2293ab9f..ac696ca13 100644 --- a/openstack_dashboard/usage/base.py +++ b/openstack_dashboard/usage/base.py @@ -12,6 +12,7 @@ from horizon import forms from horizon import messages from openstack_dashboard import api +from openstack_dashboard.usage import quotas LOG = logging.getLogger(__name__) @@ -108,7 +109,7 @@ class BaseUsage(object): def get_quotas(self): try: - self.quotas = api.nova.tenant_quota_usages(self.request) + self.quotas = quotas.tenant_quota_usages(self.request) except: exceptions.handle(self.request, _("Unable to retrieve quota information.")) diff --git a/openstack_dashboard/usage/quotas.py b/openstack_dashboard/usage/quotas.py new file mode 100644 index 000000000..2e617d76f --- /dev/null +++ b/openstack_dashboard/usage/quotas.py @@ -0,0 +1,108 @@ +from collections import defaultdict +import itertools + +from horizon import exceptions +from horizon.utils.memoized import memoized + +from openstack_dashboard.api import nova, cinder +from openstack_dashboard.api.base import is_service_enabled, QuotaSet + + +class QuotaUsage(dict): + """ Tracks quota limit, used, and available for a given set of quotas.""" + + def __init__(self): + self.usages = defaultdict(dict) + + def __getitem__(self, key): + return self.usages[key] + + def __setitem__(self, key, value): + raise NotImplemented("Directly setting QuotaUsage values is not " + "supported. Please use the add_quota and " + "tally methods.") + + def __repr__(self): + return repr(dict(self.usages)) + + def add_quota(self, quota): + """ Adds an internal tracking reference for the given quota. """ + if quota.limit is None: + # Handle "unlimited" quotas. + self.usages[quota.name]['quota'] = float("inf") + self.usages[quota.name]['available'] = float("inf") + else: + self.usages[quota.name]['quota'] = int(quota.limit) + + def tally(self, name, value): + """ Adds to the "used" metric for the given quota. """ + value = value or 0 # Protection against None. + # Start at 0 if this is the first value. + if 'used' not in self.usages[name]: + self.usages[name]['used'] = 0 + # Increment our usage and update the "available" metric. + self.usages[name]['used'] += int(value) # Fail if can't coerce to int. + self.update_available(name) + + def update_available(self, name): + """ Updates the "available" metric for the given quota. """ + available = self.usages[name]['quota'] - self.usages[name]['used'] + if available < 0: + available = 0 + self.usages[name]['available'] = available + + +def get_quota_data(request, method_name): + quotasets = [] + tenant_id = request.user.tenant_id + quotasets.append(getattr(nova, method_name)(request, tenant_id)) + if is_service_enabled(request, 'volume'): + quotasets.append(getattr(cinder, method_name)(request, tenant_id)) + qs = QuotaSet() + for quota in itertools.chain(*quotasets): + qs[quota.name] = quota.limit + return qs + + +def get_default_quota_data(request): + return get_quota_data(request, "default_quota_get") + + +def get_tenant_quota_data(request): + return get_quota_data(request, "tenant_quota_get") + + +@memoized +def tenant_quota_usages(request): + # Get our quotas and construct our usage object. + usages = QuotaUsage() + for quota in get_tenant_quota_data(request): + usages.add_quota(quota) + + # Get our usages. + floating_ips = nova.tenant_floating_ip_list(request) + flavors = dict([(f.id, f) for f in nova.flavor_list(request)]) + volumes = cinder.volume_list(request) + instances = nova.server_list(request) + # Fetch deleted flavors if necessary. + missing_flavors = [instance.flavor['id'] for instance in instances + if instance.flavor['id'] not in flavors] + for missing in missing_flavors: + if missing not in flavors: + try: + flavors[missing] = nova.flavor_get(request, missing) + except: + flavors[missing] = {} + exceptions.handle(request, ignore=True) + + usages.tally('instances', len(instances)) + usages.tally('floating_ips', len(floating_ips)) + usages.tally('volumes', len(volumes)) + usages.tally('gigabytes', sum([int(v.size) for v in volumes])) + + # Sum our usage based on the flavors of the instances. + for flavor in [flavors[instance.flavor['id']] for instance in instances]: + usages.tally('cores', getattr(flavor, 'vcpus', None)) + usages.tally('ram', getattr(flavor, 'ram', None)) + + return usages