diff --git a/openstack_dashboard/api/rest/nova.py b/openstack_dashboard/api/rest/nova.py index 5d2db901b8..2227abcc48 100644 --- a/openstack_dashboard/api/rest/nova.py +++ b/openstack_dashboard/api/rest/nova.py @@ -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): diff --git a/openstack_dashboard/static/app/core/openstack-service-api/common-test.mock.js b/openstack_dashboard/static/app/core/openstack-service-api/common-test.mock.js index ae4f95e9fd..85e6ed74c0 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/common-test.mock.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/common-test.mock.js @@ -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); + } } })(); diff --git a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js index 059ff860f9..30a66e49f3 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js @@ -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.')); + }); + } + } }()); diff --git a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.spec.js b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.spec.js index 512e0e3545..967d2f7855 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.spec.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.spec.js @@ -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", diff --git a/openstack_dashboard/test/api_tests/nova_rest_tests.py b/openstack_dashboard/test/api_tests/nova_rest_tests.py index dd32098e37..b779834659 100644 --- a/openstack_dashboard/test/api_tests/nova_rest_tests.py +++ b/openstack_dashboard/test/api_tests/nova_rest_tests.py @@ -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 #