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:
Cindy Lu 2015-02-04 18:57:59 -08:00 committed by Richard Jones
parent b7956c9c65
commit d227402f56
5 changed files with 481 additions and 0 deletions

View 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]);
}());

View File

@ -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>

View File

@ -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

View 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()
)

View 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'}]
)