Merge "Add more Nova API features for NG Instances"

This commit is contained in:
Jenkins 2016-08-18 03:32:55 +00:00 committed by Gerrit Code Review
commit 77a804ea4b
5 changed files with 750 additions and 7 deletions
openstack_dashboard
api/rest
static/app/core/openstack-service-api
test/api_tests

@ -13,12 +13,16 @@
# limitations under the License.
"""API over the nova service.
"""
from collections import OrderedDict
from django.http import HttpResponse
from django.template.defaultfilters import slugify
from django.utils import http as utils_http
from django.utils.translation import ugettext_lazy as _
from django.views import generic
from horizon import exceptions as hz_exceptions
from novaclient import exceptions
from openstack_dashboard import api
@ -27,6 +31,24 @@ from openstack_dashboard.api.rest import urls
from openstack_dashboard.api.rest import utils as rest_utils
from openstack_dashboard.usage import quotas
import six
@urls.register
class Snapshots(generic.View):
"""API for nova snapshots.
"""
url_regex = r'nova/snapshots/$'
@rest_utils.ajax(data_required=True)
def post(self, request):
instance_id = request.DATA['instance_id']
name = request.DATA['name']
result = api.nova.snapshot_create(request,
instance_id=instance_id,
name=name)
return result
@urls.register
class Keypairs(generic.View):
@ -177,6 +199,146 @@ class Limits(generic.View):
return result
@urls.register
class ServerActions(generic.View):
"""API over all server actions.
"""
url_regex = r'nova/servers/(?P<server_id>[^/]+)/actions/$'
@rest_utils.ajax()
def get(self, request, server_id):
"""Get a list of server actions.
The listing result is an object with property "items". Each item is
an action taken against the given server.
Example GET:
http://localhost/api/nova/servers/abcd/actions/
"""
actions = api.nova.instance_action_list(request, server_id)
return {'items': [s.to_dict() for s in actions]}
@urls.register
class SecurityGroups(generic.View):
"""API over all server security groups.
"""
url_regex = r'nova/servers/(?P<server_id>[^/]+)/security-groups/$'
@rest_utils.ajax()
def get(self, request, server_id):
"""Get a list of server security groups.
The listing result is an object with property "items". Each item is
security group associated with this server.
Example GET:
http://localhost/api/nova/servers/abcd/security-groups/
"""
groups = api.network.server_security_groups(request, server_id)
return {'items': [s.to_dict() for s in groups]}
@urls.register
class Volumes(generic.View):
"""API over all server volumes.
"""
url_regex = r'nova/servers/(?P<server_id>[^/]+)/volumes/$'
@rest_utils.ajax()
def get(self, request, server_id):
"""Get a list of server volumes.
The listing result is an object with property "items". Each item is
a volume.
Example GET:
http://localhost/api/nova/servers/abcd/volumes/
"""
volumes = api.nova.instance_volumes_list(request, server_id)
return {'items': [s.to_dict() for s in volumes]}
@urls.register
class RemoteConsoleInfo(generic.View):
"""API for remote console information.
"""
url_regex = r'nova/servers/(?P<server_id>[^/]+)/console-info/$'
@rest_utils.ajax()
def post(self, request, server_id):
"""Gets information about an available remote console for the given
server.
Example POST:
http://localhost/api/nova/servers/abcd/console-info/
"""
console_type = request.DATA.get('console_type', 'AUTO')
CONSOLES = OrderedDict([('VNC', api.nova.server_vnc_console),
('SPICE', api.nova.server_spice_console),
('RDP', api.nova.server_rdp_console),
('SERIAL', api.nova.server_serial_console)])
"""Get a tuple of console url and console type."""
if console_type == 'AUTO':
check_consoles = CONSOLES
else:
try:
check_consoles = {console_type: CONSOLES[console_type]}
except KeyError:
msg = _('Console type "%s" not supported.') % console_type
raise hz_exceptions.NotAvailable(msg)
# Ugly workaround due novaclient API change from 2.17 to 2.18.
try:
httpnotimplemented = exceptions.HttpNotImplemented
except AttributeError:
httpnotimplemented = exceptions.HTTPNotImplemented
for con_type, api_call in six.iteritems(check_consoles):
try:
console = api_call(request, server_id)
# If not supported, don't log it to avoid lot of errors in case
# of AUTO.
except httpnotimplemented:
continue
except Exception:
continue
if con_type == 'SERIAL':
console_url = console.url
else:
console_url = "%s&%s(%s)" % (
console.url,
utils_http.urlencode({'title': _("Console")}),
server_id)
return {"type": con_type, "url": console_url}
raise hz_exceptions.NotAvailable(_('No available console found.'))
@urls.register
class ConsoleOutput(generic.View):
"""API for console output.
"""
url_regex = r'nova/servers/(?P<server_id>[^/]+)/console-output/$'
@rest_utils.ajax()
def post(self, request, server_id):
"""Get a list of lines of console output.
The listing result is an object with property "items". Each item is
a line of output from the server.
Example GET:
http://localhost/api/nova/servers/abcd/console-output/
"""
log_length = request.DATA.get('length', 100)
console_lines = api.nova.server_console_output(request, server_id,
tail_length=log_length)
return {"lines": [x for x in console_lines.split('\n')]}
@urls.register
class Servers(generic.View):
"""API over all servers.
@ -186,7 +348,7 @@ class Servers(generic.View):
_optional_create = [
'block_device_mapping', 'block_device_mapping_v2', 'nics', 'meta',
'availability_zone', 'instance_count', 'admin_pass', 'disk_config',
'config_drive', 'scheduler_hints'
'config_drive'
]
@rest_utils.ajax()
@ -267,6 +429,27 @@ class Server(generic.View):
"""
return api.nova.server_get(request, server_id).to_dict()
@rest_utils.ajax(data_required=True)
def post(self, request, server_id):
"""Perform a change to a server
"""
operation = request.DATA.get('operation', 'none')
operations = {
'stop': api.nova.server_stop,
'start': api.nova.server_start,
'pause': api.nova.server_pause,
'unpause': api.nova.server_unpause,
'suspend': api.nova.server_suspend,
'resume': api.nova.server_resume,
'hard_reboot': lambda r, s: api.nova.server_reboot(r, s, False),
'soft_reboot': lambda r, s: api.nova.server_reboot(r, s, True),
}
return operations[operation](request, server_id)
@rest_utils.ajax()
def delete(self, request, server_id):
api.nova.server_delete(request, server_id)
@urls.register
class ServerGroups(generic.View):

@ -83,11 +83,13 @@
// The following retrieves the first argument of the first call to the
// error spy. This exposes the inner function that, when invoked,
// allows us to inspect the error call and the message given.
var innerFunc = promise.error.calls.argsFor(0)[0];
expect(innerFunc).toBeDefined();
spyOn(toastService, 'add');
innerFunc({status: 500});
expect(toastService.add).toHaveBeenCalledWith(config.messageType || 'error', config.error);
if (config.error) {
var innerFunc = promise.error.calls.argsFor(0)[0];
expect(innerFunc).toBeDefined();
spyOn(toastService, 'add');
innerFunc({status: 500});
expect(toastService.add).toHaveBeenCalledWith(config.messageType || 'error', config.error);
}
}
})();

@ -39,6 +39,11 @@
function novaAPI(apiService, toastService, $window) {
var service = {
getActionList: getActionList,
getConsoleLog: getConsoleLog,
getConsoleInfo: getConsoleInfo,
getServerVolumes: getServerVolumes,
getServerSecurityGroups: getServerSecurityGroups,
getKeypairs: getKeypairs,
createKeypair: createKeypair,
getAvailabilityZones: getAvailabilityZones,
@ -47,6 +52,15 @@
getServer: getServer,
getServers: getServers,
getServerGroups: getServerGroups,
deleteServer: deleteServer,
pauseServer: pauseServer,
unpauseServer: unpauseServer,
suspendServer: suspendServer,
resumeServer: resumeServer,
softRebootServer: softRebootServer,
hardRebootServer: hardRebootServer,
startServer: startServer,
stopServer: stopServer,
getExtensions: getExtensions,
getFlavors: getFlavors,
getFlavor: getFlavor,
@ -65,7 +79,8 @@
getDefaultQuotaSets: getDefaultQuotaSets,
setDefaultQuotaSets: setDefaultQuotaSets,
getEditableQuotas: getEditableQuotas,
updateProjectQuota: updateProjectQuota
updateProjectQuota: updateProjectQuota,
createServerSnapshot: createServerSnapshot
};
return service;
@ -264,6 +279,146 @@
});
}
/*
* @name deleteServer
* @description
* Delete a single server by ID.
*
* @param {String} serverId
* Server to delete
* @returns {Object} The result of the API call
*/
function deleteServer(serverId, suppressError) {
var promise = apiService.delete('/api/nova/servers/' + serverId);
return suppressError ? promise : promise.error(function() {
var msg = gettext('Unable to delete the server with id: %(id)s');
toastService.add('error', interpolate(msg, { id: serverId }, true));
});
}
function serverStateOperation(operation, serverId, suppressError, errMsg) {
var instruction = {"operation": operation};
var promise = apiService.post('/api/nova/servers/' + serverId, instruction);
return suppressError ? promise : promise.error(function() {
toastService.add('error', interpolate(errMsg, { id: serverId }, true));
});
}
/**
* @name startServer
* @description
* Start a single server by ID.
*
* @param {String} serverId
* Server to start
* @returns {Object} The result of the API call
*/
function startServer(serverId, suppressError) {
return serverStateOperation('start', serverId, suppressError,
gettext('Unable to start the server with id: %(id)s'));
}
/**
* @name pauseServer
* @description
* Pause a single server by ID.
*
* @param {String} serverId
* Server to pause
* @returns {Object} The result of the API call
*/
function pauseServer(serverId, suppressError) {
return serverStateOperation('pause', serverId, suppressError,
gettext('Unable to pause the server with id: %(id)s'));
}
/**
* @name unpauseServer
* @description
* Un-Pause a single server by ID.
*
* @param {String} serverId
* Server to unpause
* @returns {Object} The result of the API call
*/
function unpauseServer(serverId, suppressError) {
return serverStateOperation('unpause', serverId, suppressError,
gettext('Unable to unpause the server with id: %(id)s'));
}
/**
* @name suspendServer
* @description
* Suspend a single server by ID.
*
* @param {String} serverId
* Server to suspend
* @returns {Object} The result of the API call
*/
function suspendServer(serverId, suppressError) {
return serverStateOperation('suspend', serverId, suppressError,
gettext('Unable to suspend the server with id: %(id)s'));
}
/**
* @name resumeServer
* @description
* Resumes a single server by ID.
*
* @param {String} serverId
* Server to resume
* @returns {Object} The result of the API call
*/
function resumeServer(serverId, suppressError) {
return serverStateOperation('resume', serverId, suppressError,
gettext('Unable to resume the server with id: %(id)s'));
}
/**
* @name softRebootServer
* @description
* Soft-reboots a single server by ID.
*
* @param {String} serverId
* Server to reboot
* @returns {Object} The result of the API call
*/
function softRebootServer(serverId, suppressError) {
return serverStateOperation('soft_reboot', serverId, suppressError,
gettext('Unable to soft-reboot the server with id: %(id)s'));
}
/**
* @name hardRebootServer
* @description
* Hard-reboots a single server by ID.
*
* @param {String} serverId
* Server to reboot
* @returns {Object} The result of the API call
*/
function hardRebootServer(serverId, suppressError) {
return serverStateOperation('hard_reboot', serverId, suppressError,
gettext('Unable to hard-reboot the server with id: %(id)s'));
}
/**
* @name stopServer
* @description
* Stop a single server by ID.
*
* @param {String} serverId
* Server to stop
* @returns {Object} The result of the API call
*/
function stopServer(serverId, suppressError) {
return serverStateOperation('stop', serverId, suppressError,
gettext('Unable to stop the server with id: %(id)s'));
}
/**
* @name getExtensions
* @param {Object} config - A configuration object
@ -636,5 +791,109 @@
return getCreateKeypairUrl(keyPairName) + "?regenerate=true";
}
/**
* @name createServerSnapshot
* @param {Object} newSnapshot - The new server snapshot
* @description
* Create a server snapshot using the parameters supplied in the
* newSnapshot. The required parameters:
*
* "name", "instance_id"
* All strings
*
* @returns {Object} The result of the API call
*/
function createServerSnapshot(newSnapshot) {
return apiService.post('/api/nova/snapshots/', newSnapshot)
.error(function () {
toastService.add('error', gettext('Unable to create the server snapshot.'));
});
}
/**
* @name getActionList
* @param {String} ID - The server ID
* @description
* Retrieves a list of actions performed on the server.
*
* @returns {Object} The result of the API call
*/
function getActionList(instanceId) {
return apiService.get('/api/nova/servers/' + instanceId + '/actions/')
.error(function () {
toastService.add('error', gettext('Unable to load the server actions.'));
});
}
/**
* @name getConsoleLog
* @param {String} instanceId - The server ID
* @param {Number} length - The number of lines to retrieve (optional)
* @description
* Retrieves a list of most recent console log lines from the server.
*
* @returns {Object} The result of the API call
*/
function getConsoleLog(instanceId, length) {
var config = {};
if (length) {
config.length = length;
}
return apiService.post('/api/nova/servers/' + instanceId + '/console-output/', config)
.error(function () {
toastService.add('error', gettext('Unable to load the server console log.'));
});
}
/**
* @name getConsoleInfo
* @param {String} instanceId - The server ID
* @param {String} type - The type of console to use (optional)
* @description
* Retrieves information used to get to a remote console for the given host.
*
* @returns {Object} The result of the API call
*/
function getConsoleInfo(instanceId, type) {
var config = {};
if (type) {
config.console_type = type;
}
return apiService.post('/api/nova/servers/' + instanceId + '/console-info/', config)
.error(function () {
toastService.add('error', gettext('Unable to load the server console info.'));
});
}
/**
* @name getServerVolumes
* @param {String} instanceId - The server ID
* @description
* Retrieves information about volumes associated with the server
*
* @returns {Object} The result of the API call
*/
function getServerVolumes(instanceId) {
return apiService.get('/api/nova/servers/' + instanceId + '/volumes/')
.error(function () {
toastService.add('error', gettext('Unable to load the server volumes.'));
});
}
/**
* @name getServerSecurityGroups
* @param {String} ID - The server ID
* @description
* Retrieves information about security groups associated with the server
*
* @returns {Object} The result of the API call
*/
function getServerSecurityGroups(instanceId) {
return apiService.get('/api/nova/servers/' + instanceId + '/security-groups/')
.error(function () {
toastService.add('error', gettext('Unable to load the server security groups.'));
});
}
}
}());

@ -46,6 +46,63 @@
"path": "/api/nova/services/",
"error": "Unable to retrieve the nova services."
},
{
"func": "getConsoleLog",
"method": "post",
"path": "/api/nova/servers/123/console-output/",
"data": {
"length": 6
},
"error": "Unable to load the server console log.",
"testInput": [123, 6]
},
{
"func": "getActionList",
"method": "get",
"path": "/api/nova/servers/123/actions/",
"error": "Unable to load the server actions.",
"testInput": [123]
},
{
"func": "getConsoleLog",
"method": "post",
"path": "/api/nova/servers/123/console-output/",
"data": {},
"error": "Unable to load the server console log.",
"testInput": [123]
},
{
"func": "getConsoleInfo",
"method": "post",
"path": "/api/nova/servers/123/console-info/",
"data": {
"console_type": "VNC"
},
"error": "Unable to load the server console info.",
"testInput": [123, "VNC"]
},
{
"func": "getConsoleInfo",
"method": "post",
"path": "/api/nova/servers/123/console-info/",
"data": {},
"error": "Unable to load the server console info.",
"testInput": [123]
},
{
"func": "getServerVolumes",
"method": "get",
"path": "/api/nova/servers/123/volumes/",
"error": "Unable to load the server volumes.",
"testInput": [123]
},
{
"func": "getServerSecurityGroups",
"method": "get",
"path": "/api/nova/servers/123/security-groups/",
"error": "Unable to load the server security groups.",
"testInput": [123]
},
{
"func": "getKeypairs",
"method": "get",
@ -76,6 +133,98 @@
{}
]
},
{
"func": "deleteServer",
"method": "delete",
"path": "/api/nova/servers/12",
"error": "Unable to delete the server with id: 12",
"testInput": [12]
},
{
"func": "deleteServer",
"method": "delete",
"path": "/api/nova/servers/12",
"testInput": [12, true]
},
{
"func": "createServerSnapshot",
"method": "post",
"path": "/api/nova/snapshots/",
"data": {info: 12},
"error": "Unable to create the server snapshot.",
"testInput": [{info: 12}]
},
{
"func": "startServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'start' },
"error": "Unable to start the server with id: 12",
"testInput": [12]
},
{
"func": "stopServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'stop' },
"error": "Unable to stop the server with id: 12",
"testInput": [12]
},
{
"func": "stopServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'stop' },
"testInput": [12, true]
},
{
"func": "pauseServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'pause' },
"error": "Unable to pause the server with id: 12",
"testInput": [12]
},
{
"func": "unpauseServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'unpause' },
"error": "Unable to unpause the server with id: 12",
"testInput": [12]
},
{
"func": "suspendServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'suspend' },
"error": "Unable to suspend the server with id: 12",
"testInput": [12]
},
{
"func": "resumeServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'resume' },
"error": "Unable to resume the server with id: 12",
"testInput": [12]
},
{
"func": "softRebootServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'soft_reboot' },
"error": "Unable to soft-reboot the server with id: 12",
"testInput": [12]
},
{
"func": "hardRebootServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'hard_reboot' },
"error": "Unable to hard-reboot the server with id: 12",
"testInput": [12]
},
{
"func": "getAvailabilityZones",
"method": "get",
@ -329,6 +478,12 @@
"error": "Unable to delete the flavor with id: 42",
"testInput": [42]
},
{
"func": "deleteFlavor",
"method": "delete",
"path": "/api/nova/flavors/42/",
"testInput": [42, true]
},
{
"func": "getDefaultQuotaSets",
"method": "get",

@ -26,6 +26,150 @@ from novaclient import exceptions
class NovaRestTestCase(test.TestCase):
#
# Snapshots
#
@mock.patch.object(nova.api, 'nova')
def test_snapshots_create(self, nc):
body = '{"instance_id": "1234", "name": "foo"}'
request = self.mock_rest_request(body=body)
nc.snapshot_create.return_value = {'id': 'abcd', 'name': 'foo'}
response = nova.Snapshots().post(request)
self.assertStatusCode(response, 200)
self.assertEqual(response.json, {'id': 'abcd', 'name': 'foo'})
nc.snapshot_create.assert_called_once_with(request,
instance_id='1234',
name='foo')
#
# Server Actions
#
@mock.patch.object(nova.api, 'nova')
def test_serveractions_list(self, nc):
request = self.mock_rest_request()
nc.instance_action_list.return_value = [
mock.Mock(**{'to_dict.return_value': {'id': '1'}}),
mock.Mock(**{'to_dict.return_value': {'id': '2'}}),
]
response = nova.ServerActions().get(request, 'MegaMan')
self.assertStatusCode(response, 200)
self.assertEqual(response.json, {'items': [{'id': '1'}, {'id': '2'}]})
nc.instance_action_list.assert_called_once_with(request, 'MegaMan')
@mock.patch.object(nova.api, 'nova')
def test_server_start(self, nc):
request = self.mock_rest_request(body='{"operation": "start"}')
response = nova.Server().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
nc.server_start.assert_called_once_with(request, 'MegaMan')
@mock.patch.object(nova.api, 'nova')
def test_server_stop(self, nc):
request = self.mock_rest_request(body='{"operation": "stop"}')
response = nova.Server().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
nc.server_stop.assert_called_once_with(request, 'MegaMan')
@mock.patch.object(nova.api, 'nova')
def test_server_pause(self, nc):
request = self.mock_rest_request(body='{"operation": "pause"}')
response = nova.Server().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
nc.server_pause.assert_called_once_with(request, 'MegaMan')
@mock.patch.object(nova.api, 'nova')
def test_server_unpause(self, nc):
request = self.mock_rest_request(body='{"operation": "unpause"}')
response = nova.Server().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
nc.server_unpause.assert_called_once_with(request, 'MegaMan')
@mock.patch.object(nova.api, 'nova')
def test_server_suspend(self, nc):
request = self.mock_rest_request(body='{"operation": "suspend"}')
response = nova.Server().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
nc.server_suspend.assert_called_once_with(request, 'MegaMan')
@mock.patch.object(nova.api, 'nova')
def test_server_resume(self, nc):
request = self.mock_rest_request(body='{"operation": "resume"}')
response = nova.Server().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
nc.server_resume.assert_called_once_with(request, 'MegaMan')
@mock.patch.object(nova.api, 'nova')
def test_server_hard_reboot(self, nc):
request = self.mock_rest_request(body='{"operation": "hard_reboot"}')
response = nova.Server().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
nc.server_reboot.assert_called_once_with(request, 'MegaMan', False)
@mock.patch.object(nova.api, 'nova')
def test_server_soft_reboot(self, nc):
request = self.mock_rest_request(body='{"operation": "soft_reboot"}')
response = nova.Server().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
nc.server_reboot.assert_called_once_with(request, 'MegaMan', True)
#
# Security Groups
#
@mock.patch.object(nova.api, 'network')
def test_securitygroups_list(self, nc):
request = self.mock_rest_request()
nc.server_security_groups.return_value = [
mock.Mock(**{'to_dict.return_value': {'id': '1'}}),
mock.Mock(**{'to_dict.return_value': {'id': '2'}}),
]
response = nova.SecurityGroups().get(request, 'MegaMan')
self.assertStatusCode(response, 200)
self.assertEqual(response.json, {'items': [{'id': '1'}, {'id': '2'}]})
nc.server_security_groups.assert_called_once_with(request, 'MegaMan')
#
# Console Output
#
@mock.patch.object(nova.api, 'nova')
def test_console_output(self, nc):
request = self.mock_rest_request(body='{"length": 50}')
nc.server_console_output.return_value = "this\nis\ncool"
response = nova.ConsoleOutput().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
self.assertEqual(response.json, {'lines': ["this", "is", "cool"]})
nc.server_console_output.assert_called_once_with(request,
'MegaMan',
tail_length=50)
#
# Remote Console Info
#
@mock.patch.object(nova.api, 'nova')
def test_console_info(self, nc):
request = self.mock_rest_request(body='{"console_type": "SERIAL"}')
retval = mock.Mock(**{"url": "http://here.com"})
nc.server_serial_console.return_value = retval
response = nova.RemoteConsoleInfo().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
self.assertEqual(response.json,
{"type": "SERIAL", "url": "http://here.com"})
nc.server_serial_console.assert_called_once_with(request, 'MegaMan')
#
# Volumes
#
@mock.patch.object(nova.api, 'nova')
def test_volumes_list(self, nc):
request = self.mock_rest_request()
nc.instance_volumes_list.return_value = [
mock.Mock(**{'to_dict.return_value': {'id': '1'}}),
mock.Mock(**{'to_dict.return_value': {'id': '2'}}),
]
response = nova.Volumes().get(request, 'MegaMan')
self.assertStatusCode(response, 200)
self.assertEqual(response.json, {'items': [{'id': '1'}, {'id': '2'}]})
nc.instance_volumes_list.assert_called_once_with(request, 'MegaMan')
#
# Keypairs
#