Nova REST API for angular front end
This the nova service REST API needed to support the create instance wizard work. This patch now follows the new verb usage patterns. Partially Implements: blueprint launch-instance-redesign Co-Authored-By: Michael Hagedorn <mike.hagedorn@hp.com> Co-Authored-By: Richard Jones <r1chardj0n3s@gmail.com> Co-Authored-By: Travis Tripp <travis.tripp@hp.com> Change-Id: I0096665aed325addafdcc985d7460ac8b21cb902
This commit is contained in:
parent
b7956c9c65
commit
d227402f56
151
horizon/static/horizon/js/angular/services/hz.api.nova.js
Normal file
151
horizon/static/horizon/js/angular/services/hz.api.nova.js
Normal file
@ -0,0 +1,151 @@
|
||||
/*
|
||||
Copyright 2014, Rackspace, US, 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.
|
||||
*/
|
||||
/*global angular,horizon*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name hz.api.novaAPI
|
||||
* @description Provides access to Nova APIs.
|
||||
*/
|
||||
function NovaAPI(apiService) {
|
||||
|
||||
// Keypairs
|
||||
|
||||
/**
|
||||
* @name hz.api.novaAPI.getKeypairs
|
||||
* @description
|
||||
* Get a list of keypairs.
|
||||
*
|
||||
* The listing result is an object with property "items". Each item is
|
||||
* a keypair.
|
||||
*/
|
||||
this.getKeypairs = function() {
|
||||
return apiService.get('/api/nova/keypairs/')
|
||||
.error(function () {
|
||||
horizon.alert('error', gettext('Unable to retrieve keypairs.'));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @name hz.api.novaAPI.createKeypair
|
||||
* @description
|
||||
* Create a new keypair. This returns the new keypair object on success.
|
||||
*
|
||||
* @param {Object} newKeypair
|
||||
* The keypair to create.
|
||||
*
|
||||
* @param {string} newKeypair.name
|
||||
* The name of the new keypair. Required.
|
||||
*
|
||||
* @param {string} newKeypair.public_key
|
||||
* The public key. Optional.
|
||||
*/
|
||||
this.createKeypair = function(newKeypair) {
|
||||
return apiService.post('/api/nova/keypairs/', newKeypair)
|
||||
.error(function () {
|
||||
horizon.alert('error', gettext('Unable to create the keypair.'));
|
||||
});
|
||||
};
|
||||
|
||||
// Availability Zones
|
||||
|
||||
/**
|
||||
* @name hz.api.novaAPI.getAvailabilityZones
|
||||
* @description
|
||||
* Get a list of Availability Zones.
|
||||
*
|
||||
* The listing result is an object with property "items". Each item is
|
||||
* an availability zone.
|
||||
*/
|
||||
this.getAvailabilityZones = function() {
|
||||
return apiService.get('/api/nova/availzones/')
|
||||
.error(function () {
|
||||
horizon.alert('error',
|
||||
gettext('Unable to retrieve availability zones.'));
|
||||
});
|
||||
};
|
||||
|
||||
// Limits
|
||||
|
||||
/**
|
||||
* @name hz.api.novaAPI.getLimits
|
||||
* @description
|
||||
* Returns current limits.
|
||||
*
|
||||
* @example
|
||||
* The following is an example response:
|
||||
* {
|
||||
* "maxImageMeta": 128,
|
||||
* "maxPersonality": 5,
|
||||
* "maxPersonalitySize": 10240,
|
||||
* "maxSecurityGroupRules": 20,
|
||||
* "maxSecurityGroups": 10,
|
||||
* "maxServerGroupMembers": 10,
|
||||
* "maxServerGroups": 10,
|
||||
* "maxServerMeta": 128,
|
||||
* "maxTotalCores": 20,
|
||||
* "maxTotalFloatingIps": 10,
|
||||
* "maxTotalInstances": 10,
|
||||
* "maxTotalKeypairs": 100,
|
||||
* "maxTotalRAMSize": 51200,
|
||||
* "totalCoresUsed": 1,
|
||||
* "totalFloatingIpsUsed": 0,
|
||||
* "totalInstancesUsed": 1,
|
||||
* "totalRAMUsed": 512,
|
||||
* "totalSecurityGroupsUsed": 1,
|
||||
* "totalServerGroupsUsed": 0
|
||||
* }
|
||||
*/
|
||||
this.getLimits = function() {
|
||||
return apiService.get('/api/nova/limits/')
|
||||
.error(function () {
|
||||
horizon.alert('error', gettext('Unable to retrieve limits.'));
|
||||
});
|
||||
};
|
||||
|
||||
// Servers
|
||||
|
||||
/**
|
||||
* @name hz.api.novaAPI.createServer
|
||||
* @description
|
||||
* Create a server using the parameters supplied in the
|
||||
* newServer. The required parameters:
|
||||
*
|
||||
* "name", "source_id", "flavor_id", "key_name", "user_data"
|
||||
* All strings
|
||||
* "security_groups"
|
||||
* An array of one or more objects with a "name" attribute.
|
||||
*
|
||||
* Other parameters are accepted as per the underlying novaclient:
|
||||
* "block_device_mapping", "block_device_mapping_v2", "nics", "meta",
|
||||
* "availability_zone", "instance_count", "admin_pass", "disk_config",
|
||||
* "config_drive"
|
||||
*
|
||||
* This returns the new server object on success.
|
||||
*/
|
||||
this.createServer = function(newServer) {
|
||||
return apiService.post('/api/nova/servers/', newServer)
|
||||
.error(function () {
|
||||
horizon.alert('error', gettext('Unable to create the server.'));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
angular.module('hz.api')
|
||||
.service('novaAPI', ['apiService', NovaAPI]);
|
||||
}());
|
@ -22,6 +22,7 @@
|
||||
<script src='{{ STATIC_URL }}horizon/js/angular/services/hz.api.service.js'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/js/angular/services/hz.api.keystone.js'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/js/angular/services/hz.api.glance.js'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/js/angular/services/hz.api.nova.js'></script>
|
||||
|
||||
<script src='{{ STATIC_URL }}angular/widget.module.js'></script>
|
||||
<script src='{{ STATIC_URL }}angular/help-panel/help-panel.js'></script>
|
||||
|
@ -24,3 +24,4 @@ in https://wiki.openstack.org/wiki/APIChangeGuidelines.
|
||||
# import REST API modules here
|
||||
import glance #flake8: noqa
|
||||
import keystone #flake8: noqa
|
||||
import nova #flake8: noqa
|
||||
|
175
openstack_dashboard/api/rest/nova.py
Normal file
175
openstack_dashboard/api/rest/nova.py
Normal file
@ -0,0 +1,175 @@
|
||||
|
||||
# Copyright 2014, Rackspace, US, 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.
|
||||
"""API over the nova service.
|
||||
"""
|
||||
|
||||
import urllib
|
||||
|
||||
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
|
||||
|
||||
|
||||
@urls.register
|
||||
class Keypairs(generic.View):
|
||||
"""API for nova keypairs.
|
||||
"""
|
||||
url_regex = r'nova/keypairs/$'
|
||||
|
||||
@rest_utils.ajax()
|
||||
def get(self, request):
|
||||
"""Get a list of keypairs associated with the current logged-in
|
||||
account.
|
||||
|
||||
The listing result is an object with property "items".
|
||||
"""
|
||||
result = api.nova.keypair_list(request)
|
||||
return {'items': [u.to_dict() for u in result]}
|
||||
|
||||
@rest_utils.ajax(data_required=True)
|
||||
def post(self, request):
|
||||
"""Create a keypair.
|
||||
|
||||
Create a keypair using the parameters supplied in the POST
|
||||
application/json object. The parameters are:
|
||||
|
||||
:param name: the name to give the keypair
|
||||
:param public_key: (optional) a key to import
|
||||
|
||||
This returns the new keypair object on success.
|
||||
"""
|
||||
if 'public_key' in request.DATA:
|
||||
new = api.nova.keypair_import(request, request.DATA['name'],
|
||||
request.DATA['public_key'])
|
||||
else:
|
||||
new = api.nova.keypair_create(request, request.DATA['name'])
|
||||
return rest_utils.CreatedResponse(
|
||||
'/api/nova/keypairs/%s' % urllib.quote(new.name),
|
||||
new.to_dict()
|
||||
)
|
||||
|
||||
|
||||
@urls.register
|
||||
class AvailabilityZones(generic.View):
|
||||
"""API for nova availability zones.
|
||||
"""
|
||||
url_regex = r'nova/availzones/$'
|
||||
|
||||
@rest_utils.ajax()
|
||||
def get(self, request):
|
||||
"""Get a list of availability zones.
|
||||
|
||||
The following get parameters may be passed in the GET
|
||||
request:
|
||||
|
||||
:param detailed: If this equals "true" then the result will
|
||||
include more detail.
|
||||
|
||||
The listing result is an object with property "items".
|
||||
"""
|
||||
detailed = request.GET.get('detailed') == 'true'
|
||||
result = api.nova.availability_zone_list(request, detailed)
|
||||
return {'items': [u.to_dict() for u in result]}
|
||||
|
||||
|
||||
@urls.register
|
||||
class Limits(generic.View):
|
||||
"""API for nova limits.
|
||||
"""
|
||||
url_regex = r'nova/limits/$'
|
||||
|
||||
@rest_utils.ajax()
|
||||
def get(self, request):
|
||||
"""Get an object describing the current project limits.
|
||||
|
||||
Note: the Horizon API doesn't support any other project (tenant) but
|
||||
the underlying client does...
|
||||
|
||||
The following get parameters may be passed in the GET
|
||||
request:
|
||||
|
||||
:param reserved: This may be set to "true" but it's not
|
||||
clear what the result of that is.
|
||||
|
||||
The result is an object with limits as properties.
|
||||
"""
|
||||
reserved = request.GET.get('reserved') == 'true'
|
||||
result = api.nova.tenant_absolute_limits(request, reserved)
|
||||
return result
|
||||
|
||||
|
||||
@urls.register
|
||||
class Servers(generic.View):
|
||||
"""API over all servers.
|
||||
"""
|
||||
url_regex = r'nova/servers/$'
|
||||
|
||||
_optional_create = [
|
||||
'block_device_mapping', 'block_device_mapping_v2', 'nics', 'meta',
|
||||
'availability_zone', 'instance_count', 'admin_pass', 'disk_config',
|
||||
'config_drive'
|
||||
]
|
||||
|
||||
@rest_utils.ajax(data_required=True)
|
||||
def post(self, request):
|
||||
"""Create a server.
|
||||
|
||||
Create a server using the parameters supplied in the POST
|
||||
application/json object. The required parameters as specified by
|
||||
the underlying novaclient are:
|
||||
|
||||
:param name: The new server name.
|
||||
:param source_id: The ID of the image to use.
|
||||
:param flavor_id: The ID of the flavor to use.
|
||||
:param key_name: (optional extension) name of previously created
|
||||
keypair to inject into the instance.
|
||||
:param user_data: user data to pass to be exposed by the metadata
|
||||
server this can be a file type object as well or a
|
||||
string.
|
||||
:param security_groups: An array of one or more objects with a "name"
|
||||
attribute.
|
||||
|
||||
Other parameters are accepted as per the underlying novaclient:
|
||||
"block_device_mapping", "block_device_mapping_v2", "nics", "meta",
|
||||
"availability_zone", "instance_count", "admin_pass", "disk_config",
|
||||
"config_drive"
|
||||
|
||||
This returns the new server object on success.
|
||||
"""
|
||||
try:
|
||||
args = (
|
||||
request,
|
||||
request.DATA['name'],
|
||||
request.DATA['source_id'],
|
||||
request.DATA['flavor_id'],
|
||||
request.DATA['key_name'],
|
||||
request.DATA['user_data'],
|
||||
request.DATA['security_groups'],
|
||||
)
|
||||
except KeyError as e:
|
||||
raise rest_utils.AjaxError(400, 'missing required parameter '
|
||||
"'%s'" % e.args[0])
|
||||
kw = {}
|
||||
for name in self._optional_create:
|
||||
if name in request.DATA:
|
||||
kw[name] = request.DATA[name]
|
||||
|
||||
new = api.nova.server_create(*args, **kw)
|
||||
return rest_utils.CreatedResponse(
|
||||
'/api/nova/servers/%s' % urllib.quote(new.id),
|
||||
new.to_dict()
|
||||
)
|
153
openstack_dashboard/test/api_tests/nova_rest_tests.py
Normal file
153
openstack_dashboard/test/api_tests/nova_rest_tests.py
Normal file
@ -0,0 +1,153 @@
|
||||
# Copyright 2014, Rackspace, US, 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.
|
||||
import mock
|
||||
import unittest2
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from openstack_dashboard.api.rest import nova
|
||||
|
||||
from rest_test_utils import construct_request # noqa
|
||||
|
||||
|
||||
class NovaRestTestCase(unittest2.TestCase):
|
||||
def assertStatusCode(self, response, expected_code):
|
||||
if response.status_code == expected_code:
|
||||
return
|
||||
self.fail('status code %r != %r: %s' % (response.status_code,
|
||||
expected_code,
|
||||
response.content))
|
||||
|
||||
#
|
||||
# Keypairs
|
||||
#
|
||||
@mock.patch.object(nova.api, 'nova')
|
||||
def test_keypair_get(self, nc):
|
||||
request = construct_request()
|
||||
nc.keypair_list.return_value = [
|
||||
mock.Mock(**{'to_dict.return_value': {'id': 'one'}}),
|
||||
mock.Mock(**{'to_dict.return_value': {'id': 'two'}}),
|
||||
]
|
||||
response = nova.Keypairs().get(request)
|
||||
self.assertStatusCode(response, 200)
|
||||
self.assertEqual(response.content,
|
||||
'{"items": [{"id": "one"}, {"id": "two"}]}')
|
||||
nc.keypair_list.assert_called_once_with(request)
|
||||
|
||||
@mock.patch.object(nova.api, 'nova')
|
||||
def test_keypair_create(self, nc):
|
||||
request = construct_request(body='''{"name": "Ni!"}''')
|
||||
new = nc.keypair_create.return_value
|
||||
new.to_dict.return_value = {'name': 'Ni!', 'public_key': 'sekrit'}
|
||||
new.name = 'Ni!'
|
||||
with mock.patch.object(settings, 'DEBUG', True):
|
||||
response = nova.Keypairs().post(request)
|
||||
self.assertStatusCode(response, 201)
|
||||
self.assertEqual(response.content,
|
||||
'{"name": "Ni!", "public_key": "sekrit"}')
|
||||
self.assertEqual(response['location'], '/api/nova/keypairs/Ni%21')
|
||||
nc.keypair_create.assert_called_once_with(request, 'Ni!')
|
||||
|
||||
@mock.patch.object(nova.api, 'nova')
|
||||
def test_keypair_import(self, nc):
|
||||
request = construct_request(body='''
|
||||
{"name": "Ni!", "public_key": "hi"}
|
||||
''')
|
||||
new = nc.keypair_import.return_value
|
||||
new.to_dict.return_value = {'name': 'Ni!', 'public_key': 'hi'}
|
||||
new.name = 'Ni!'
|
||||
with mock.patch.object(settings, 'DEBUG', True):
|
||||
response = nova.Keypairs().post(request)
|
||||
self.assertStatusCode(response, 201)
|
||||
self.assertEqual(response.content,
|
||||
'{"name": "Ni!", "public_key": "hi"}')
|
||||
self.assertEqual(response['location'], '/api/nova/keypairs/Ni%21')
|
||||
nc.keypair_import.assert_called_once_with(request, 'Ni!', 'hi')
|
||||
|
||||
#
|
||||
# Availability Zones
|
||||
#
|
||||
def test_availzone_get_brief(self):
|
||||
self._test_availzone_get(False)
|
||||
|
||||
def test_availzone_get_detailed(self):
|
||||
self._test_availzone_get(True)
|
||||
|
||||
@mock.patch.object(nova.api, 'nova')
|
||||
def _test_availzone_get(self, detail, nc):
|
||||
if detail:
|
||||
request = construct_request(GET={'detailed': 'true'})
|
||||
else:
|
||||
request = construct_request(GET={})
|
||||
nc.availability_zone_list.return_value = [
|
||||
mock.Mock(**{'to_dict.return_value': {'id': 'one'}}),
|
||||
mock.Mock(**{'to_dict.return_value': {'id': 'two'}}),
|
||||
]
|
||||
response = nova.AvailabilityZones().get(request)
|
||||
self.assertStatusCode(response, 200)
|
||||
self.assertEqual(response.content,
|
||||
'{"items": [{"id": "one"}, {"id": "two"}]}')
|
||||
nc.availability_zone_list.assert_called_once_with(request, detail)
|
||||
|
||||
#
|
||||
# Limits
|
||||
#
|
||||
def test_limits_get_not_reserved(self):
|
||||
self._test_limits_get(False)
|
||||
|
||||
def test_limits_get_reserved(self):
|
||||
self._test_limits_get(True)
|
||||
|
||||
@mock.patch.object(nova.api, 'nova')
|
||||
def _test_limits_get(self, reserved, nc):
|
||||
if reserved:
|
||||
request = construct_request(GET={'reserved': 'true'})
|
||||
else:
|
||||
request = construct_request(GET={})
|
||||
nc.tenant_absolute_limits.return_value = {'id': 'one'}
|
||||
response = nova.Limits().get(request)
|
||||
self.assertStatusCode(response, 200)
|
||||
nc.tenant_absolute_limits.assert_called_once_with(request, reserved)
|
||||
self.assertEqual(response.content, '{"id": "one"}')
|
||||
|
||||
#
|
||||
# Servers
|
||||
#
|
||||
@mock.patch.object(nova.api, 'nova')
|
||||
def test_server_create_missing(self, nc):
|
||||
request = construct_request(body='''{"name": "hi"}''')
|
||||
response = nova.Servers().post(request)
|
||||
self.assertStatusCode(response, 400)
|
||||
self.assertEqual(response.content,
|
||||
'"missing required parameter \'source_id\'"')
|
||||
nc.server_create.assert_not_called()
|
||||
|
||||
@mock.patch.object(nova.api, 'nova')
|
||||
def test_server_create_basic(self, nc):
|
||||
request = construct_request(body='''{"name": "Ni!",
|
||||
"source_id": "image123", "flavor_id": "flavor123",
|
||||
"key_name": "sekrit", "user_data": "base64 yes",
|
||||
"security_groups": [{"name": "root"}]}
|
||||
''')
|
||||
new = nc.server_create.return_value
|
||||
new.to_dict.return_value = {'id': 'server123'}
|
||||
new.id = 'server123'
|
||||
response = nova.Servers().post(request)
|
||||
self.assertStatusCode(response, 201)
|
||||
self.assertEqual(response.content, '{"id": "server123"}')
|
||||
self.assertEqual(response['location'], '/api/nova/servers/server123')
|
||||
nc.server_create.assert_called_once_with(
|
||||
request, 'Ni!', 'image123', 'flavor123', 'sekrit', 'base64 yes',
|
||||
[{'name': 'root'}]
|
||||
)
|
Loading…
Reference in New Issue
Block a user