API Services for Flavors Panel

Adding the api services needed by the flavor panel
for the sub flows

To test set DISABLED = False in _2081_admin_flavors_panel.py

Co-Authored-By: Rajat Vig <rajatv@thoughtworks.com>
Co-Authored-By: Errol Pais <epais@thoughtworks.com>
Co-Authored-By: Kristine Brown <kbrown@thoughtworks.com>
Co-Authored-By: Kyle Olivo <keolivo@thoughtworks.com>
Co-Authored-By: Yakira Dixon <ydixon@thoughtworks.com>
Co-Authored-By: Dan Siwiec <dsiwiec@thoughtworks.com>

Change-Id: Iad97739203a05cce3c971cc8458cd2eb67e117d3
Partially-Implements: blueprint ng-flavors
This commit is contained in:
Rajat Vig 2015-10-08 12:25:26 -07:00
parent 80bbc35944
commit 0903fd20ff
4 changed files with 400 additions and 9 deletions

View File

@ -1,4 +1,3 @@
# Copyright 2014, Rackspace, US, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
@ -352,6 +351,33 @@ class Flavors(generic.View):
result['items'].append(d)
return result
@rest_utils.ajax(data_required=True)
def post(self, request):
flavor_access = request.DATA.get('flavor_access', [])
flavor_id = request.DATA['id']
is_public = not flavor_access
flavor = api.nova.flavor_create(request,
name=request.DATA['name'],
memory=request.DATA['ram'],
vcpu=request.DATA['vcpus'],
disk=request.DATA['disk'],
ephemeral=request
.DATA['OS-FLV-EXT-DATA:ephemeral'],
swap=request.DATA['swap'],
flavorid=flavor_id,
is_public=is_public
)
for project in flavor_access:
api.nova.add_tenant_to_flavor(
request, flavor.id, project.get('id'))
return rest_utils.CreatedResponse(
'/api/nova/flavors/%s' % flavor.id,
flavor.to_dict()
)
@urls.register
class Flavor(generic.View):
@ -368,14 +394,64 @@ class Flavor(generic.View):
Example GET:
http://localhost/api/nova/flavors/1
"""
get_extras = request.GET.get('get_extras')
get_extras = bool(get_extras and get_extras.lower() == 'true')
get_extras = self.extract_boolean(request, 'get_extras')
get_access_list = self.extract_boolean(request, 'get_access_list')
flavor = api.nova.flavor_get(request, flavor_id, get_extras=get_extras)
result = flavor.to_dict()
# Bug: nova API stores and returns empty string when swap equals 0
# https://bugs.launchpad.net/nova/+bug/1408954
if 'swap' in result and result['swap'] == '':
result['swap'] = 0
if get_extras:
result['extras'] = flavor.extras
if get_access_list and not flavor.is_public:
access_list = [item.tenant_id for item in
api.nova.flavor_access_list(request, flavor_id)]
result['access-list'] = access_list
return result
@rest_utils.ajax()
def delete(self, request, flavor_id):
api.nova.flavor_delete(request, flavor_id)
@rest_utils.ajax(data_required=True)
def patch(self, request, flavor_id):
flavor_access = request.DATA.get('flavor_access', [])
is_public = not flavor_access
# Grab any existing extra specs, because flavor edit is currently
# implemented as a delete followed by a create.
extras_dict = api.nova.flavor_get_extras(request, flavor_id, raw=True)
# Mark the existing flavor as deleted.
api.nova.flavor_delete(request, flavor_id)
# Then create a new flavor with the same name but a new ID.
# This is in the same try/except block as the delete call
# because if the delete fails the API will error out because
# active flavors can't have the same name.
flavor = api.nova.flavor_create(request,
name=request.DATA['name'],
memory=request.DATA['ram'],
vcpu=request.DATA['vcpus'],
disk=request.DATA['disk'],
ephemeral=request
.DATA['OS-FLV-EXT-DATA:ephemeral'],
swap=request.DATA['swap'],
flavorid=flavor_id,
is_public=is_public
)
for project in flavor_access:
api.nova.add_tenant_to_flavor(
request, flavor.id, project.get('id'))
if extras_dict:
api.nova.flavor_extra_set(request, flavor.id, extras_dict)
def extract_boolean(self, request, name):
bool_string = request.GET.get(name)
return bool(bool_string and bool_string.lower() == 'true')
@urls.register
class FlavorExtraSpecs(generic.View):

View File

@ -53,7 +53,10 @@
getInstanceMetadata: getInstanceMetadata,
editInstanceMetadata: editInstanceMetadata,
getCreateKeypairUrl: getCreateKeypairUrl,
getRegenerateKeypairUrl: getRegenerateKeypairUrl
getRegenerateKeypairUrl: getRegenerateKeypairUrl,
createFlavor: createFlavor,
updateFlavor: updateFlavor,
deleteFlavor: deleteFlavor
};
return service;
@ -314,15 +317,66 @@
* @param {boolean} getExtras (optional)
* Also retrieve the extra specs for the flavor.
*/
function getFlavor(id, getExtras) {
function getFlavor(id, getExtras, getAccessList) {
var config = {'params': {}};
if (getExtras) { config.params.get_extras = 'true'; }
return apiService.get('/api/nova/flavors/' + id, config)
if (getAccessList) { config.params.get_access_list = 'true'; }
return apiService.get('/api/nova/flavors/' + id + '/' , config)
.error(function () {
toastService.add('error', gettext('Unable to retrieve the flavor.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.nova.createFlavor
* @description
* Create a single flavor.
* @param {flavor} flavor
* Flavor to create
*/
function createFlavor(flavor) {
return apiService.post('/api/nova/flavors/', flavor)
.error(function () {
toastService.add('error', gettext('Unable to create the flavor.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.nova.updateFlavor
* @description
* Update a single flavor.
* @param {flavor} flavor
* Flavor to update
*/
function updateFlavor(flavor) {
return apiService.patch('/api/nova/flavors/' + flavor.id + '/', flavor)
.error(function () {
toastService.add('error', gettext('Unable to update the flavor.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.nova.deleteFlavor
* @description
* Delete a single flavor by ID.
*
* @param {String} flavorId
* Flavor to delete
*
* @param {boolean} suppressError
* If passed in, this will not show the default error handling
* (horizon alert). The glance API may not have metadata definitions
* enabled.
*/
function deleteFlavor(flavorId, suppressError) {
var promise = apiService.delete('/api/nova/flavors/' + flavorId + '/');
return suppressError ? promise : promise.error(function() {
toastService.add('error', gettext('Unable to delete the flavor with id: ') + flavorId);
});
}
/**
* @name horizon.app.core.openstack-service-api.nova.getFlavorExtraSpecs
* @description

View File

@ -185,7 +185,7 @@
{
"func": "getFlavor",
"method": "get",
"path": "/api/nova/flavors/42",
"path": "/api/nova/flavors/42/",
"data": {
"params": {
"get_extras": "true"
@ -200,7 +200,24 @@
{
"func": "getFlavor",
"method": "get",
"path": "/api/nova/flavors/42",
"path": "/api/nova/flavors/42/",
"data": {
"params": {
"get_extras": "true",
"get_access_list": "true"
}
},
"error": "Unable to retrieve the flavor.",
"testInput": [
42,
true,
true
]
},
{
"func": "getFlavor",
"method": "get",
"path": "/api/nova/flavors/42/",
"data": {
"params": {}
},
@ -275,6 +292,37 @@
"testInput": [
42, {a: '1', b: '2'}, ['c', 'd']
]
},
{
"func": "createFlavor",
"method": "post",
"path": "/api/nova/flavors/",
"data": 42,
"error": "Unable to create the flavor.",
"testInput": [
42
]
},
{
"func": "updateFlavor",
"method": "patch",
"path": "/api/nova/flavors/42/",
"data": {
id: 42
},
"error": "Unable to update the flavor.",
"testInput": [
{
id: 42
}
]
},
{
"func": "deleteFlavor",
"method": "delete",
"path": "/api/nova/flavors/42/",
"error": "Unable to delete the flavor with id: 42",
"testInput": [42]
}
];

View File

@ -14,6 +14,7 @@
import mock
from django.conf import settings
from json import loads as to_json
from openstack_dashboard import api
from openstack_dashboard.api.rest import nova
@ -281,6 +282,27 @@ class NovaRestTestCase(test.TestCase):
#
# Flavors
#
@mock.patch.object(nova.api, 'nova')
def test_flavor_get_single_with_access_list(self, nc):
request = self.mock_rest_request(GET={'get_access_list': 'tRuE'})
nc.flavor_get.return_value.to_dict.return_value = {'name': '1'}
nc.flavor_get.return_value.is_public = False
nc.flavor_access_list.return_value = [
mock.Mock(**{'tenant_id': '11'}),
mock.Mock(**{'tenant_id': '22'}),
]
response = nova.Flavor().get(request, "1")
self.assertStatusCode(response, 200)
self.assertEqual(to_json(response.content.decode('utf-8')),
to_json('{"access-list": ["11", "22"], "name": "1"}'))
nc.flavor_get.assert_called_once_with(request, "1",
get_extras=False)
def test_get_extras_no(self):
self._test_flavor_get_single(get_extras=False)
@ -310,6 +332,197 @@ class NovaRestTestCase(test.TestCase):
nc.flavor_get.assert_called_once_with(request, "1",
get_extras=get_extras)
@mock.patch.object(nova.api, 'nova')
def test_flavor_get_single_with_swap_set_to_empty(self, nc):
request = self.mock_rest_request()
nc.flavor_get.return_value\
.to_dict.return_value = {'name': '1', 'swap': ''}
response = nova.Flavor().get(request, "1")
self.assertStatusCode(response, 200)
self.assertEqual(to_json(response.content.decode('utf-8')),
to_json('{"name": "1", "swap": 0}'))
@mock.patch.object(nova.api, 'nova')
def test_flavor_delete(self, nc):
request = self.mock_rest_request()
nova.Flavor().delete(request, "1")
nc.flavor_delete.assert_called_once_with(request, "1")
@mock.patch.object(nova.api, 'nova')
def test_flavor_create(self, nc):
flavor_req_data = '{"name": "flavor", ' \
'"ram": 12, ' \
'"vcpus": 1, ' \
'"disk": 2, ' \
'"OS-FLV-EXT-DATA:ephemeral": 3, ' \
'"swap": 4, ' \
'"id": "123"' \
'}'
nc.flavor_create.return_value = mock.Mock(**{
'id': '123',
'to_dict.return_value': {'id': '123', 'name': 'flavor'}
})
flavor_data = {'name': 'flavor',
'memory': 12,
'vcpu': 1,
'disk': 2,
'ephemeral': 3,
'swap': 4,
'flavorid': '123',
'is_public': True}
request = self.mock_rest_request(body=flavor_req_data)
response = nova.Flavors().post(request)
self.assertStatusCode(response, 201)
self.assertEqual(response['location'], '/api/nova/flavors/123')
nc.flavor_create.assert_called_once_with(request, **flavor_data)
@mock.patch.object(nova.api, 'nova')
def test_flavor_create_with_access_list(self, nc):
flavor_req_data = '{"name": "flavor", ' \
'"ram": 12, ' \
'"vcpus": 1, ' \
'"disk": 2, ' \
'"OS-FLV-EXT-DATA:ephemeral": 3, ' \
'"swap": 4, ' \
'"id": "123", ' \
'"flavor_access": [{"id":"1", "name":"test"}]' \
'}'
nc.flavor_create.return_value = mock.Mock(**{
'id': '1234',
'to_dict.return_value': {'id': '1234', 'name': 'flavor'}
})
flavor_data = {'name': 'flavor',
'memory': 12,
'vcpu': 1,
'disk': 2,
'ephemeral': 3,
'swap': 4,
'flavorid': '123',
'is_public': False}
request = self.mock_rest_request(body=flavor_req_data)
response = nova.Flavors().post(request)
self.assertStatusCode(response, 201)
self.assertEqual(response['location'], '/api/nova/flavors/1234')
nc.flavor_create.assert_called_once_with(request, **flavor_data)
nc.add_tenant_to_flavor.assert_called_once_with(request, '1234', '1')
@mock.patch.object(nova.api, 'nova')
def test_flavor_update(self, nc):
flavor_req_data = '{"name": "flavor", ' \
'"ram": 12, ' \
'"vcpus": 1, ' \
'"disk": 2, ' \
'"OS-FLV-EXT-DATA:ephemeral": 3, ' \
'"swap": 4' \
'}'
nc.flavor_create.return_value = mock.Mock(**{
'id': '123',
'to_dict.return_value': {'id': '123', 'name': 'flavor'}
})
flavor_data = {'name': 'flavor',
'memory': 12,
'vcpu': 1,
'disk': 2,
'ephemeral': 3,
'swap': 4,
'flavorid': '123',
'is_public': True}
request = self.mock_rest_request(body=flavor_req_data)
response = nova.Flavor().patch(request, '123')
self.assertStatusCode(response, 204)
nc.flavor_delete.assert_called_once_with(request, '123')
nc.flavor_create.assert_called_once_with(request, **flavor_data)
@mock.patch.object(nova.api, 'nova')
def test_flavor_update_with_extras(self, nc):
flavor_req_data = '{"name": "flavor", ' \
'"ram": 12, ' \
'"vcpus": 1, ' \
'"disk": 2, ' \
'"OS-FLV-EXT-DATA:ephemeral": 3, ' \
'"swap": 4' \
'}'
extra_dict = mock.Mock()
nc.flavor_get_extras.return_value = extra_dict
nc.flavor_create.return_value = mock.Mock(**{
'id': '1234',
'to_dict.return_value': {'id': '1234', 'name': 'flavor'}
})
flavor_data = {'name': 'flavor',
'memory': 12,
'vcpu': 1,
'disk': 2,
'ephemeral': 3,
'swap': 4,
'flavorid': '123',
'is_public': True}
request = self.mock_rest_request(body=flavor_req_data)
response = nova.Flavor().patch(request, '123')
self.assertStatusCode(response, 204)
nc.flavor_delete.assert_called_once_with(request, '123')
nc.flavor_create.assert_called_once_with(request, **flavor_data)
nc.flavor_get_extras.assert_called_once_with(request, '123', raw=True)
nc.flavor_extra_set.assert_called_once_with(request, '1234',
extra_dict)
@mock.patch.object(nova.api, 'nova')
def test_flavor_update_with_access_list(self, nc):
flavor_req_data = '{"name": "flavor", ' \
'"ram": 12, ' \
'"vcpus": 1, ' \
'"disk": 2, ' \
'"OS-FLV-EXT-DATA:ephemeral": 3, ' \
'"swap": 4, ' \
'"flavor_access": [{"id":"1", "name":"test"}]' \
'}'
nc.flavor_create.return_value = mock.Mock(**{
'id': '1234',
'to_dict.return_value': {'id': '1234', 'name': 'flavor'}
})
flavor_data = {'name': 'flavor',
'memory': 12,
'vcpu': 1,
'disk': 2,
'ephemeral': 3,
'swap': 4,
'flavorid': '123',
'is_public': False}
request = self.mock_rest_request(body=flavor_req_data)
response = nova.Flavor().patch(request, '123')
self.assertStatusCode(response, 204)
nc.flavor_delete.assert_called_once_with(request, '123')
nc.flavor_create.assert_called_once_with(request, **flavor_data)
nc.add_tenant_to_flavor.assert_called_once_with(request, '1234', '1')
@mock.patch.object(nova.api, 'nova')
def _test_flavor_list_public(self, nc, is_public=None):
if is_public: