Adding Identity NGProjects API

This patch adds some projects-related endpoints to the Nova,
Cinder and Neutron REST/Angular APIs.

These changes are needed for the angular Identity Projects
actions that handle mainly project quota updates.

Partially-Implements: blueprint angularize-identity-projects

Change-Id: I98b8eb9e5e7bfb0d49a77c00115fe62d412abe21
This commit is contained in:
Paulo Ewerton Gomes Fragoso 2015-06-16 18:21:23 +00:00 committed by Paulo Ewerton
parent 20bfc93dc5
commit 3945b24062
12 changed files with 520 additions and 7 deletions

View File

@ -300,3 +300,32 @@ class DefaultQuotaSets(generic.View):
api.cinder.default_quota_update(request, **cinder_data)
else:
raise rest_utils.AjaxError(501, _('Service Cinder is disabled.'))
@urls.register
class QuotaSets(generic.View):
"""API for setting quotas for a given project.
"""
url_regex = r'cinder/quota-sets/(?P<project_id>[0-9a-f]+)$'
@rest_utils.ajax(data_required=True)
def patch(self, request, project_id):
"""Update a single project quota data.
The PATCH data should be an application/json object with the
attributes to set to new quota values.
This method returns HTTP 204 (no content) on success.
"""
# Filters cinder quota fields
disabled_quotas = quotas.get_disabled_quotas(request)
if api.cinder.is_volume_service_enabled():
cinder_data = {
key: request.DATA[key] for key in quotas.CINDER_QUOTA_FIELDS
if key not in disabled_quotas
}
api.cinder.tenant_quota_update(request, project_id, **cinder_data)
else:
raise rest_utils.AjaxError(501, _('Service Cinder is disabled.'))

View File

@ -15,11 +15,13 @@
"""API over the neutron service.
"""
from django.utils.translation import ugettext_lazy as _
from django.views import generic
from openstack_dashboard import api
from openstack_dashboard.api.rest import urls
from openstack_dashboard.api.rest import utils as rest_utils
from openstack_dashboard.usage import quotas
@urls.register
@ -173,3 +175,62 @@ class Extensions(generic.View):
"""
result = api.neutron.list_extensions(request)
return {'items': [e for e in result]}
class DefaultQuotaSets(generic.View):
"""API for getting default quotas for neutron
"""
url_regex = r'neutron/quota-sets/defaults/$'
@rest_utils.ajax()
def get(self, request):
if api.base.is_service_enabled(request, 'network'):
quota_set = api.neutron.tenant_quota_get(
request, request.user.tenant_id)
result = [{
'display_name': quotas.QUOTA_NAMES.get(
quota.name,
quota.name.replace('_', ' ').title()
) + '',
'name': quota.name,
'limit': quota.limit
} for quota in quota_set]
return {'items': result}
else:
raise rest_utils.AjaxError(501, _('Service Neutron is disabled.'))
@urls.register
class QuotasSets(generic.View):
"""API for setting quotas of a given project.
"""
url_regex = r'neutron/quotas-sets/(?P<project_id>[0-9a-f]+)$'
@rest_utils.ajax(data_required=True)
def patch(self, request, project_id):
"""Update a single project quota data.
The PATCH data should be an application/json object with the
attributes to set to new quota values.
This method returns HTTP 204 (no content) on success.
"""
# Filters only neutron quota fields
disabled_quotas = quotas.get_disabled_quotas(request)
if api.base.is_service_enabled(request, 'network') and \
api.neutron.is_extension_supported(request, 'quotas'):
neutron_data = {
key: request.DATA[key] for key in quotas.NEUTRON_QUOTA_FIELDS
if key not in disabled_quotas
}
api.neutron.tenant_quota_update(request,
project_id,
**neutron_data)
else:
message = _('Service Neutron is disabled or quotas extension not '
'available.')
raise rest_utils.AjaxError(501, message)

View File

@ -572,3 +572,51 @@ class DefaultQuotaSets(generic.View):
api.nova.default_quota_update(request, **nova_data)
else:
raise rest_utils.AjaxError(501, _('Service Nova is disabled.'))
@urls.register
class EditableQuotaSets(generic.View):
"""API for editable quotas.
"""
url_regex = r'nova/quota-sets/editable/$'
@rest_utils.ajax()
def get(self, request):
"""Get a list of editable quota fields.
The listing result is an object with property "items". Each item
is an editable quota. Returns an empty list in case no editable
quota is found.
"""
disabled_quotas = quotas.get_disabled_quotas(request)
editable_quotas = [quota for quota in quotas.QUOTA_FIELDS
if quota not in disabled_quotas]
return {'items': editable_quotas}
@urls.register
class QuotaSets(generic.View):
"""API for setting quotas for a given project.
"""
url_regex = r'nova/quota-sets/(?P<project_id>[0-9a-f]+)$'
@rest_utils.ajax(data_required=True)
def patch(self, request, project_id):
"""Update a single project quota data.
The PATCH data should be an application/json object with the
attributes to set to new quota values.
This method returns HTTP 204 (no content) on success.
"""
disabled_quotas = quotas.get_disabled_quotas(request)
if api.base.is_service_enabled(request, 'compute'):
nova_data = {
key: request.DATA[key] for key in quotas.NOVA_QUOTA_FIELDS
if key not in disabled_quotas
}
api.nova.tenant_quota_update(request, project_id, **nova_data)
else:
raise rest_utils.AjaxError(501, _('Service Nova is disabled.'))

View File

@ -47,7 +47,8 @@
getAbsoluteLimits: getAbsoluteLimits,
getServices: getServices,
getDefaultQuotaSets: getDefaultQuotaSets,
setDefaultQuotaSets: setDefaultQuotaSets
setDefaultQuotaSets: setDefaultQuotaSets,
updateProjectQuota: updateProjectQuota
};
return service;
@ -199,8 +200,7 @@
var config = params ? {'params': params} : {};
return apiService.get('/api/cinder/volumesnapshots/', config)
.error(function () {
toastService.add('error',
gettext('Unable to retrieve the volume snapshots.'));
toastService.add('error', gettext('Unable to retrieve the volume snapshots.'));
});
}
@ -322,5 +322,24 @@
});
}
// Quota Sets
/**
* @name updateProjectQuota
* @description
* Update a single project quota data.
* @param {application/json} quota
* A JSON object with the atributes to set to new quota values.
* @param {string} projectId
* Specifies the id of the project that'll have the quota data updated.
*/
function updateProjectQuota(quota, projectId) {
var url = '/api/cinder/quota-sets/' + projectId;
return apiService.patch(url, quota)
.error(function() {
toastService.add('error', gettext('Unable to update project quota data.'));
});
}
}
}());

View File

@ -158,6 +158,13 @@
method: 'patch',
path: '/api/cinder/quota-sets/defaults/',
error: 'Unable to set the default quotas.'
},
{ func: 'updateProjectQuota',
method: 'patch',
path: '/api/cinder/quota-sets/42',
data: {'volumes': 42},
error: 'Unable to update project quota data.',
testInput: [{'volumes': 42}, 42]
}
];

View File

@ -41,7 +41,9 @@
createSubnet: createSubnet,
getPorts: getPorts,
getAgents: getAgents,
getExtensions: getExtensions
getExtensions: getExtensions,
getDefaultQuotaSets: getDefaultQuotaSets,
updateProjectQuota: updateProjectQuota
};
return service;
@ -297,6 +299,43 @@
toastService.add('error', gettext('Unable to retrieve the extensions.'));
});
}
// Default Quota Sets
/**
* @name getDefaultQuotaSets
* @description
* Get default quotasets
*
* The listing result is an object with property "items." Each item is
* a quota.
*
*/
function getDefaultQuotaSets() {
return apiService.get('/api/neutron/quota-sets/defaults/')
.error(function() {
toastService.add('error', gettext('Unable to retrieve the default quotas.'));
});
}
// Quotas Extension
/**
* @name updateProjectQuota
* @description
* Update a single project quota data.
* @param {application/json} quota
* A JSON object with the atributes to set to new quota values.
* @param {string} projectId
* Specifies the id of the project that'll have the quota data updated.
*/
function updateProjectQuota(quota, projectId) {
var url = '/api/neutron/quotas-sets/' + projectId;
return apiService.patch(url, quota)
.error(function() {
toastService.add('error', gettext('Unable to update project quota data.'));
});
}
}
}());

View File

@ -104,6 +104,27 @@
"method": "get",
"path": "/api/neutron/extensions/",
"error": "Unable to retrieve the extensions."
},
{
"func": "getDefaultQuotaSets",
"method": "get",
"path": "/api/neutron/quota-sets/defaults/",
"error": "Unable to retrieve the default quotas."
},
{
"func": "updateProjectQuota",
"method": "patch",
"path": "/api/neutron/quotas-sets/42",
"data": {
"network": 42
},
"error": "Unable to update project quota data.",
"testInput": [
{
"network": 42
},
42
]
}
];

View File

@ -62,7 +62,9 @@
updateFlavor: updateFlavor,
deleteFlavor: deleteFlavor,
getDefaultQuotaSets: getDefaultQuotaSets,
setDefaultQuotaSets: setDefaultQuotaSets
setDefaultQuotaSets: setDefaultQuotaSets,
getEditableQuotas: getEditableQuotas,
updateProjectQuota: updateProjectQuota
};
return service;
@ -516,7 +518,7 @@
// Default Quota Sets
/**
* @name horizon.app.core.openstack-service-api.nova.getDefaultQuotaSets
* @name getDefaultQuotaSets
* @description
* Get default quotasets
*
@ -532,7 +534,7 @@
}
/**
* @name horizon.app.core.openstack-service-api.nova.setDefaultQuotaSets
* @name setDefaultQuotaSets
* @description
* Set default quotasets
*
@ -544,6 +546,40 @@
});
}
// Quota Sets
/**
* @name getEditableQuotas
* @description
* Get a list of editable quota fields.
* The listing result is an object with property "items." Each item is
* an editable quota field.
*
*/
function getEditableQuotas() {
return apiService.get('/api/nova/quota-sets/editable/')
.error(function() {
toastService.add('error', gettext('Unable to retrieve the editable quotas.'));
});
}
/**
* @name updateProjectQuota
* @description
* Update a single project quota data.
* @param {application/json} quota
* A JSON object with the atributes to set to new quota values.
* @param {string} projectId
* Specifies the id of the project that'll have the quota data updated.
*/
function updateProjectQuota(quota, projectId) {
var url = '/api/nova/quota-sets/' + projectId;
return apiService.patch(url, quota)
.error(function() {
toastService.add('error', gettext('Unable to update project quota data.'));
});
}
/**
* @ngdoc function
* @name getCreateKeypairUrl

View File

@ -343,6 +343,27 @@
],
"path": "/api/nova/quota-sets/defaults/",
"error": "Unable to set the default quotas."
},
{
"func": "getEditableQuotas",
"method": "get",
"path": "/api/nova/quota-sets/editable/",
"error": "Unable to retrieve the editable quotas."
},
{
"func": "updateProjectQuota",
"method": "patch",
"path": "/api/nova/quota-sets/42",
"data": {
"cores": 42
},
"error": "Unable to update project quota data.",
"testInput": [
{
"cores": 42
},
42
]
}
];

View File

@ -346,3 +346,55 @@ class CinderRestTestCase(test.TestCase):
'"Service Cinder is disabled."')
cc.default_quota_update.assert_not_called()
@mock.patch.object(cinder.api, 'cinder')
@mock.patch.object(cinder, 'quotas')
def test_quota_sets_patch(self, qc, cc):
quota_set = self.cinder_quotas.list()[0]
quota_data = {}
for quota in quota_set:
quota_data[quota.name] = quota.limit
request = self.mock_rest_request(body='''
{"volumes": "15", "snapshots": "5000",
"gigabytes": "5", "cores": "10"}
''')
qc.get_disabled_quotas.return_value = []
qc.CINDER_QUOTA_FIELDS = (n for n in quota_data)
cc.is_volume_service_enabled.return_value = True
response = cinder.QuotaSets().patch(request, 'spam123')
self.assertStatusCode(response, 204)
self.assertEqual(response.content.decode('utf-8'), '')
cc.tenant_quota_update.assert_called_once_with(request, 'spam123',
volumes='15',
snapshots='5000',
gigabytes='5')
@mock.patch.object(cinder.api, 'cinder')
@mock.patch.object(cinder, 'quotas')
def test_quota_sets_when_service_is_disabled(self, qc, cc):
quota_set = self.cinder_quotas.list()[0]
quota_data = {}
for quota in quota_set:
quota_data[quota.name] = quota.limit
request = self.mock_rest_request(body='''
{"volumes": "15", "snapshots": "5000",
"gigabytes": "5", "cores": "10"}
''')
qc.get_disabled_quotas.return_value = []
qc.CINDER_QUOTA_FIELDS = (n for n in quota_data)
cc.is_volume_service_enabled.return_value = False
response = cinder.QuotaSets().patch(request, 'spam123')
self.assertStatusCode(response, 501)
self.assertEqual(response.content.decode('utf-8'),
'"Service Cinder is disabled."')
cc.tenant_quota_update.assert_not_called()

View File

@ -16,6 +16,7 @@
import mock
from openstack_dashboard import api
from openstack_dashboard.api import base
from openstack_dashboard.api.rest import neutron
from openstack_dashboard.test import helpers as test
from openstack_dashboard.test.test_data import neutron_data
@ -174,6 +175,109 @@ class NeutronExtensionsTestCase(test.TestCase):
nc.list_extensions.assert_called_once_with(request)
class NeutronDefaultQuotasTestCase(test.TestCase):
@test.create_stubs({base: ('is_service_enabled',)})
@mock.patch.object(neutron.api, 'neutron')
def test_quotas_sets_defaults_get_when_service_is_enabled(self, client):
filters = {'user': {'tenant_id': 'tenant'}}
request = self.mock_rest_request(**{'GET': dict(filters)})
base.is_service_enabled(request, 'network').AndReturn(True)
client.tenant_quota_get.return_value = [
base.Quota("network", 100),
base.Quota("q2", 101)]
self.mox.ReplayAll()
response = neutron.DefaultQuotaSets().get(request)
self.assertStatusCode(response, 200)
self.assertItemsCollectionEqual(response, [
{'limit': 100, 'display_name': 'Networks', 'name': 'network'},
{'limit': 101, 'display_name': 'Q2', 'name': 'q2'}])
client.tenant_quota_get.assert_called_once_with(
request,
request.user.tenant_id)
@test.create_stubs({neutron.api.base: ('is_service_enabled',)})
@mock.patch.object(neutron.api, 'neutron')
def test_quota_sets_defaults_get_when_service_is_disabled(self, client):
filters = {'user': {'tenant_id': 'tenant'}}
request = self.mock_rest_request(**{'GET': dict(filters)})
base.is_service_enabled(request, 'network').AndReturn(False)
self.mox.ReplayAll()
response = neutron.DefaultQuotaSets().get(request)
self.assertStatusCode(response, 501)
self.assertEqual(response.content.decode('utf-8'),
'"Service Neutron is disabled."')
client.tenant_quota_get.assert_not_called()
class NeutronQuotaSetsTestCase(test.TestCase):
def setUp(self):
super(NeutronQuotaSetsTestCase, self).setUp()
quota_set = self.neutron_quotas.list()[0]
self._quota_data = {}
for quota in quota_set:
self._quota_data[quota.name] = quota.limit
@mock.patch.object(neutron, 'quotas')
@mock.patch.object(neutron.api, 'neutron')
@mock.patch.object(neutron.api, 'base')
def test_quotas_sets_patch(self, bc, nc, qc):
request = self.mock_rest_request(body='''
{"network": "5", "subnet": "5", "port": "50",
"router": "5", "floatingip": "50",
"security_group": "5", "security_group_rule": "50",
"volumes": "5", "cores": "50"}
''')
qc.get_disabled_quotas.return_value = []
qc.NEUTRON_QUOTA_FIELDS = (n for n in self._quota_data)
bc.is_service_enabled.return_value = True
nc.is_extension_supported.return_value = True
response = neutron.QuotasSets().patch(request, 'spam123')
self.assertStatusCode(response, 204)
self.assertEqual(response.content.decode('utf-8'), '')
nc.tenant_quota_update.assert_called_once_with(
request, 'spam123', network='5',
subnet='5', port='50', router='5',
floatingip='50', security_group='5',
security_group_rule='50')
@mock.patch.object(neutron, 'quotas')
@mock.patch.object(neutron.api, 'neutron')
@mock.patch.object(neutron.api, 'base')
def test_quotas_sets_patch_when_service_is_disabled(self, bc, nc, qc):
request = self.mock_rest_request(body='''
{"network": "5", "subnet": "5", "port": "50",
"router": "5", "floatingip": "50",
"security_group": "5", "security_group_rule": "50",
"volumes": "5", "cores": "50"}
''')
qc.get_disabled_quotas.return_value = []
qc.NEUTRON_QUOTA_FIELDS = (n for n in self._quota_data)
bc.is_service_enabled.return_value = False
response = neutron.QuotasSets().patch(request, 'spam123')
message = \
'"Service Neutron is disabled or quotas extension not available."'
self.assertStatusCode(response, 501)
self.assertEqual(response.content.decode('utf-8'), message)
nc.tenant_quota_update.assert_not_called()
def mock_obj_to_dict(r):
return mock.Mock(**{'to_dict.return_value': r})

View File

@ -769,3 +769,79 @@ class NovaRestTestCase(test.TestCase):
'"Service Nova is disabled."')
nc.default_quota_update.assert_not_called()
@mock.patch.object(nova, 'quotas')
@mock.patch.object(nova.api, 'nova')
def test_editable_quotas_get(self, nc, qc):
disabled_quotas = ['floating_ips', 'fixed_ips',
'security_groups', 'security_group_rules']
editable_quotas = ['cores', 'volumes', 'network', 'fixed_ips']
qc.get_disabled_quotas.return_value = disabled_quotas
qc.QUOTA_FIELDS = editable_quotas
request = self.mock_rest_request()
response = nova.EditableQuotaSets().get(request)
self.assertStatusCode(response, 200)
self.assertItemsCollectionEqual(response,
['cores', 'volumes', 'network'])
@mock.patch.object(nova.api, 'nova')
@mock.patch.object(nova.api, 'base')
@mock.patch.object(nova, 'quotas')
def test_quota_sets_patch(self, qc, bc, nc):
quota_data = dict(cores='15', instances='5',
ram='50000', metadata_items='150',
injected_files='5',
injected_file_content_bytes='10240',
floating_ips='50', fixed_ips='5',
security_groups='10',
security_group_rules='100')
request = self.mock_rest_request(body='''
{"cores": "15", "ram": "50000", "instances": "5",
"metadata_items": "150", "injected_files": "5",
"injected_file_content_bytes": "10240", "floating_ips": "50",
"fixed_ips": "5", "security_groups": "10" ,
"security_group_rules": "100", "volumes": "10"}
''')
qc.get_disabled_quotas.return_value = []
qc.NOVA_QUOTA_FIELDS = (n for n in quota_data)
bc.is_service_enabled.return_value = True
response = nova.QuotaSets().patch(request, 'spam123')
self.assertStatusCode(response, 204)
self.assertEqual(response.content.decode('utf-8'), '')
nc.tenant_quota_update.assert_called_once_with(
request, 'spam123', **quota_data)
@mock.patch.object(nova.api, 'nova')
@mock.patch.object(nova.api, 'base')
@mock.patch.object(nova, 'quotas')
def test_quota_sets_patch_when_service_is_disabled(self, qc, bc, nc):
quota_data = dict(cores='15', instances='5',
ram='50000', metadata_items='150',
injected_files='5',
injected_file_content_bytes='10240',
floating_ips='50', fixed_ips='5',
security_groups='10',
security_group_rules='100')
request = self.mock_rest_request(body='''
{"cores": "15", "ram": "50000", "instances": "5",
"metadata_items": "150", "injected_files": "5",
"injected_file_content_bytes": "10240", "floating_ips": "50",
"fixed_ips": "5", "security_groups": "10" ,
"security_group_rules": "100", "volumes": "10"}
''')
qc.get_disabled_quotas.return_value = []
qc.NOVA_QUOTA_FIELDS = (n for n in quota_data)
bc.is_service_enabled.return_value = False
response = nova.QuotaSets().patch(request, 'spam123')
self.assertStatusCode(response, 501)
self.assertEqual(response.content.decode('utf-8'),
'"Service Nova is disabled."')
nc.tenant_quota_update.assert_not_called()