diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json index e20a39d5c4..34503af2b4 100644 --- a/doc/api_samples/versions/versions-get-resp.json +++ b/doc/api_samples/versions/versions-get-resp.json @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.5", + "version": "2.6", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/v3/api_samples/os-remote-consoles/v2.6/create-vnc-console-req.json b/doc/v3/api_samples/os-remote-consoles/v2.6/create-vnc-console-req.json new file mode 100644 index 0000000000..f050019c9f --- /dev/null +++ b/doc/v3/api_samples/os-remote-consoles/v2.6/create-vnc-console-req.json @@ -0,0 +1,6 @@ +{ + "remote_console": { + "protocol": "vnc", + "type": "novnc" + } +} diff --git a/doc/v3/api_samples/os-remote-consoles/v2.6/create-vnc-console-resp.json b/doc/v3/api_samples/os-remote-consoles/v2.6/create-vnc-console-resp.json new file mode 100644 index 0000000000..b427a69022 --- /dev/null +++ b/doc/v3/api_samples/os-remote-consoles/v2.6/create-vnc-console-resp.json @@ -0,0 +1,7 @@ +{ + "remote_console": { + "protocol": "vnc", + "type": "novnc", + "url": "http://example.com:6080/vnc_auto.html?token=b60bcfc3-5fd4-4d21-986c-e83379107819" + } +} diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index a5e7369c4e..21b80a4d0d 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -44,6 +44,7 @@ REST_API_VERSION_HISTORY = """REST API Version History: Exposes delete_on_termination for os-extended-volumes * 2.4 - Exposes reserved field in os-fixed-ips. * 2.5 - Allow server search option ip6 for non-admin + * 2.6 - Consolidate the APIs for getting remote consoles """ # The minimum and maximum versions of the API supported @@ -52,7 +53,7 @@ REST_API_VERSION_HISTORY = """REST API Version History: # Note(cyeoh): This only applies for the v2.1 API once microversions # support is fully merged. It does not affect the V2 API. _MIN_API_VERSION = "2.1" -_MAX_API_VERSION = "2.5" +_MAX_API_VERSION = "2.6" DEFAULT_API_VERSION = _MIN_API_VERSION diff --git a/nova/api/openstack/compute/plugins/v3/remote_consoles.py b/nova/api/openstack/compute/plugins/v3/remote_consoles.py index b0d2cc33cb..3428f2654d 100644 --- a/nova/api/openstack/compute/plugins/v3/remote_consoles.py +++ b/nova/api/openstack/compute/plugins/v3/remote_consoles.py @@ -30,8 +30,13 @@ authorize = extensions.os_compute_authorizer(ALIAS) class RemoteConsolesController(wsgi.Controller): def __init__(self, *args, **kwargs): self.compute_api = compute.API(skip_policy_check=True) + self.handlers = {'vnc': self.compute_api.get_vnc_console, + 'spice': self.compute_api.get_spice_console, + 'rdp': self.compute_api.get_rdp_console, + 'serial': self.compute_api.get_serial_console} super(RemoteConsolesController, self).__init__(*args, **kwargs) + @wsgi.Controller.api_version("2.1", "2.5") @extensions.expected_errors((400, 404, 409, 501)) @wsgi.action('os-getVNCConsole') @validation.schema(remote_consoles.get_vnc_console) @@ -59,6 +64,7 @@ class RemoteConsolesController(wsgi.Controller): return {'console': {'type': console_type, 'url': output['url']}} + @wsgi.Controller.api_version("2.1", "2.5") @extensions.expected_errors((400, 404, 409, 501)) @wsgi.action('os-getSPICEConsole') @validation.schema(remote_consoles.get_spice_console) @@ -86,6 +92,7 @@ class RemoteConsolesController(wsgi.Controller): return {'console': {'type': console_type, 'url': output['url']}} + @wsgi.Controller.api_version("2.1", "2.5") @extensions.expected_errors((400, 404, 409, 501)) @wsgi.action('os-getRDPConsole') @validation.schema(remote_consoles.get_rdp_console) @@ -115,6 +122,7 @@ class RemoteConsolesController(wsgi.Controller): return {'console': {'type': console_type, 'url': output['url']}} + @wsgi.Controller.api_version("2.1", "2.5") @extensions.expected_errors((400, 404, 409, 501)) @wsgi.action('os-getSerialConsole') @validation.schema(remote_consoles.get_serial_console) @@ -144,6 +152,34 @@ class RemoteConsolesController(wsgi.Controller): return {'console': {'type': console_type, 'url': output['url']}} + @wsgi.Controller.api_version("2.6") + @extensions.expected_errors((400, 404, 409, 501)) + @validation.schema(remote_consoles.create_v26) + def create(self, req, server_id, body): + context = req.environ['nova.context'] + authorize(context) + instance = common.get_instance(self.compute_api, context, server_id) + protocol = body['remote_console']['protocol'] + console_type = body['remote_console']['type'] + try: + handler = self.handlers.get(protocol) + output = handler(context, instance, console_type) + return {'remote_console': {'protocol': protocol, + 'type': console_type, + 'url': output['url']}} + + except exception.InstanceNotFound as e: + raise webob.exc.HTTPNotFound(explanation=e.format_message()) + except exception.InstanceNotReady as e: + raise webob.exc.HTTPConflict(explanation=e.format_message()) + except (exception.ConsoleTypeUnavailable, + exception.ImageSerialPortNumberInvalid, + exception.ImageSerialPortNumberExceedFlavorValue, + exception.SocketPortRangeExhaustedException) as e: + raise webob.exc.HTTPBadRequest(explanation=e.format_message()) + except NotImplementedError: + common.raise_feature_not_supported() + class RemoteConsoles(extensions.V3APIExtensionBase): """Interactive Console support.""" @@ -157,4 +193,10 @@ class RemoteConsoles(extensions.V3APIExtensionBase): return [extension] def get_resources(self): - return [] + parent = {'member_name': 'server', + 'collection_name': 'servers'} + resources = [ + extensions.ResourceExtension( + 'remote-consoles', RemoteConsolesController(), parent=parent, + member_name='remote-console')] + return resources diff --git a/nova/api/openstack/compute/schemas/v3/remote_consoles.py b/nova/api/openstack/compute/schemas/v3/remote_consoles.py index 8859014c76..dcacab9515 100644 --- a/nova/api/openstack/compute/schemas/v3/remote_consoles.py +++ b/nova/api/openstack/compute/schemas/v3/remote_consoles.py @@ -87,3 +87,25 @@ get_serial_console = { 'required': ['os-getSerialConsole'], 'additionalProperties': False, } + +create_v26 = { + 'type': 'object', + 'properties': { + 'remote_console': { + 'type': 'object', + 'properties': { + 'protocol': { + 'enum': ['vnc', 'spice', 'rdp', 'serial'], + }, + 'type': { + 'enum': ['novnc', 'xvpvnc', 'rdp-html5', + 'spice-html5', 'serial'], + }, + }, + 'required': ['protocol', 'type'], + 'additionalProperties': False, + }, + }, + 'required': ['remote_console'], + 'additionalProperties': False, +} diff --git a/nova/api/openstack/rest_api_version_history.rst b/nova/api/openstack/rest_api_version_history.rst index 916839edc5..e221144d54 100644 --- a/nova/api/openstack/rest_api_version_history.rst +++ b/nova/api/openstack/rest_api_version_history.rst @@ -64,3 +64,29 @@ user documentation. for non-admins, as the filter option is silently discarded. There is no reason to treat ip6 different from ip, though, so we just add this option to the allowed list. + +2.6 +--- + + A new API for getting remote console is added:: + + POST /servers//remote-consoles + { + "remote_console": { + "protocol": ["vnc"|"rdp"|"serial"|"spice"], + "type": ["novnc"|"xpvnc"|"rdp-html5"|"spice-html5"|"serial"] + } + } + + Example response:: + + { + "remote_console": { + "protocol": "vnc", + "type": "novnc", + "url": "http://example.com:6080/vnc_auto.html?token=XYZ" + } + } + + The old APIs 'os-getVNCConsole', 'os-getSPICEConsole', 'os-getSerialConsole' + and 'os-getRDPConsole' are removed. diff --git a/nova/tests/functional/api_samples/versions/versions-get-resp.json.tpl b/nova/tests/functional/api_samples/versions/versions-get-resp.json.tpl index e20a39d5c4..34503af2b4 100644 --- a/nova/tests/functional/api_samples/versions/versions-get-resp.json.tpl +++ b/nova/tests/functional/api_samples/versions/versions-get-resp.json.tpl @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.5", + "version": "2.6", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/nova/tests/functional/v3/api_samples/os-remote-consoles/v2.6/create-vnc-console-req.json.tpl b/nova/tests/functional/v3/api_samples/os-remote-consoles/v2.6/create-vnc-console-req.json.tpl new file mode 100644 index 0000000000..f050019c9f --- /dev/null +++ b/nova/tests/functional/v3/api_samples/os-remote-consoles/v2.6/create-vnc-console-req.json.tpl @@ -0,0 +1,6 @@ +{ + "remote_console": { + "protocol": "vnc", + "type": "novnc" + } +} diff --git a/nova/tests/functional/v3/api_samples/os-remote-consoles/v2.6/create-vnc-console-resp.json.tpl b/nova/tests/functional/v3/api_samples/os-remote-consoles/v2.6/create-vnc-console-resp.json.tpl new file mode 100644 index 0000000000..a61f711c06 --- /dev/null +++ b/nova/tests/functional/v3/api_samples/os-remote-consoles/v2.6/create-vnc-console-resp.json.tpl @@ -0,0 +1,7 @@ +{ + "remote_console": { + "protocol": "vnc", + "type": "novnc", + "url": "%(url)s" + } +} diff --git a/nova/tests/functional/v3/test_remote_consoles.py b/nova/tests/functional/v3/test_remote_consoles.py index 6833259e90..6b77827525 100644 --- a/nova/tests/functional/v3/test_remote_consoles.py +++ b/nova/tests/functional/v3/test_remote_consoles.py @@ -83,3 +83,27 @@ class ConsolesSampleJsonTests(test_servers.ServersSampleBase): "((ws?):((//)|(\\\\))+([\w\d:#@%/;$()~_?\+-=\\\.&](#!)?)*)" self._verify_response('get-serial-console-post-resp', subs, response, 200) + + +class ConsolesV26SampleJsonTests(test_servers.ServersSampleBase): + extension_name = "os-remote-consoles" + _api_version = 'v3' + + def setUp(self): + super(ConsolesV26SampleJsonTests, self).setUp() + self.http_regex = "(https?://)([\w\d:#@%/;$()~_?\+-=\\\.&](#!)?)*" + + def test_create_console(self): + # NOTE(rgerganov): set temporary to None to avoid duplicating server + # templates in the v2.6 folder + ConsolesV26SampleJsonTests.request_api_version = None + uuid = self._post_server() + ConsolesV26SampleJsonTests.request_api_version = '2.6' + + body = {'protocol': 'vnc', 'type': 'novnc'} + response = self._do_post('servers/%s/remote-consoles' % uuid, + 'create-vnc-console-req', body, + api_version='2.6') + subs = self._get_regexes() + subs["url"] = self.http_regex + self._verify_response('create-vnc-console-resp', subs, response, 200) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_consoles.py b/nova/tests/unit/api/openstack/compute/contrib/test_consoles.py index d1376c784d..fc61ddee18 100644 --- a/nova/tests/unit/api/openstack/compute/contrib/test_consoles.py +++ b/nova/tests/unit/api/openstack/compute/contrib/test_consoles.py @@ -16,6 +16,7 @@ import mock import webob +from nova.api.openstack import api_version_request from nova.api.openstack.compute.contrib import consoles \ as console_v2 from nova.api.openstack.compute.plugins.v3 import remote_consoles \ @@ -428,6 +429,150 @@ class ConsolesExtensionTestV21(test.NoDBTestCase): self.assertTrue(get_serial_console.called) +class ConsolesExtensionTestV26(test.NoDBTestCase): + def setUp(self): + super(ConsolesExtensionTestV26, self).setUp() + self.req = fakes.HTTPRequest.blank('') + self.context = self.req.environ['nova.context'] + self.req.api_version_request = api_version_request.APIVersionRequest( + '2.6') + self.controller = console_v21.RemoteConsolesController() + + @mock.patch.object(compute_api.API, 'get', return_value='fake_instance') + def test_create_vnc_console(self, mock_get): + mock_handler = mock.MagicMock() + mock_handler.return_value = {'url': "http://fake"} + self.controller.handlers['vnc'] = mock_handler + + body = {'remote_console': {'protocol': 'vnc', 'type': 'novnc'}} + output = self.controller.create(self.req, fakes.FAKE_UUID, body=body) + self.assertEqual({'remote_console': {'protocol': 'vnc', + 'type': 'novnc', + 'url': 'http://fake'}}, output) + mock_handler.assert_called_once_with(self.context, 'fake_instance', + 'novnc') + + @mock.patch.object(compute_api.API, 'get', return_value='fake_instance') + def test_create_spice_console(self, mock_get): + mock_handler = mock.MagicMock() + mock_handler.return_value = {'url': "http://fake"} + self.controller.handlers['spice'] = mock_handler + + body = {'remote_console': {'protocol': 'spice', + 'type': 'spice-html5'}} + output = self.controller.create(self.req, fakes.FAKE_UUID, body=body) + self.assertEqual({'remote_console': {'protocol': 'spice', + 'type': 'spice-html5', + 'url': 'http://fake'}}, output) + mock_handler.assert_called_once_with(self.context, 'fake_instance', + 'spice-html5') + + @mock.patch.object(compute_api.API, 'get', return_value='fake_instance') + def test_create_rdp_console(self, mock_get): + mock_handler = mock.MagicMock() + mock_handler.return_value = {'url': "http://fake"} + self.controller.handlers['rdp'] = mock_handler + + body = {'remote_console': {'protocol': 'rdp', 'type': 'rdp-html5'}} + output = self.controller.create(self.req, fakes.FAKE_UUID, body=body) + self.assertEqual({'remote_console': {'protocol': 'rdp', + 'type': 'rdp-html5', + 'url': 'http://fake'}}, output) + mock_handler.assert_called_once_with(self.context, 'fake_instance', + 'rdp-html5') + + @mock.patch.object(compute_api.API, 'get', return_value='fake_instance') + def test_create_serial_console(self, mock_get): + mock_handler = mock.MagicMock() + mock_handler.return_value = {'url': "http://fake"} + self.controller.handlers['serial'] = mock_handler + + body = {'remote_console': {'protocol': 'serial', 'type': 'serial'}} + output = self.controller.create(self.req, fakes.FAKE_UUID, body=body) + self.assertEqual({'remote_console': {'protocol': 'serial', + 'type': 'serial', + 'url': 'http://fake'}}, output) + mock_handler.assert_called_once_with(self.context, 'fake_instance', + 'serial') + + @mock.patch.object(compute_api.API, 'get', return_value='fake_instance') + def test_create_console_instance_not_ready(self, mock_get): + mock_handler = mock.MagicMock() + mock_handler.side_effect = exception.InstanceNotReady( + instance_id='xxx') + self.controller.handlers['vnc'] = mock_handler + + body = {'remote_console': {'protocol': 'vnc', 'type': 'novnc'}} + self.assertRaises(webob.exc.HTTPConflict, self.controller.create, + self.req, fakes.FAKE_UUID, body=body) + + @mock.patch.object(compute_api.API, 'get', return_value='fake_instance') + def test_create_console_unavailable(self, mock_get): + mock_handler = mock.MagicMock() + mock_handler.side_effect = exception.ConsoleTypeUnavailable( + console_type='vnc') + self.controller.handlers['vnc'] = mock_handler + + body = {'remote_console': {'protocol': 'vnc', 'type': 'novnc'}} + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + self.req, fakes.FAKE_UUID, body=body) + self.assertTrue(mock_handler.called) + + @mock.patch.object(compute_api.API, 'get', return_value='fake_instance') + def test_create_console_not_found(self, mock_get): + mock_handler = mock.MagicMock() + mock_handler.side_effect = exception.InstanceNotFound( + instance_id='xxx') + self.controller.handlers['vnc'] = mock_handler + + body = {'remote_console': {'protocol': 'vnc', 'type': 'novnc'}} + self.assertRaises(webob.exc.HTTPNotFound, self.controller.create, + self.req, fakes.FAKE_UUID, body=body) + + @mock.patch.object(compute_api.API, 'get', return_value='fake_instance') + def test_create_console_not_implemented(self, mock_get): + mock_handler = mock.MagicMock() + mock_handler.side_effect = NotImplementedError() + self.controller.handlers['vnc'] = mock_handler + + body = {'remote_console': {'protocol': 'vnc', 'type': 'novnc'}} + self.assertRaises(webob.exc.HTTPNotImplemented, self.controller.create, + self.req, fakes.FAKE_UUID, body=body) + + @mock.patch.object(compute_api.API, 'get', return_value='fake_instance') + def test_create_console_nport_invalid(self, mock_get): + mock_handler = mock.MagicMock() + mock_handler.side_effect = exception.ImageSerialPortNumberInvalid( + num_ports='x', property="hw_serial_port_count") + self.controller.handlers['serial'] = mock_handler + + body = {'remote_console': {'protocol': 'serial', 'type': 'serial'}} + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + self.req, fakes.FAKE_UUID, body=body) + + @mock.patch.object(compute_api.API, 'get', return_value='fake_instance') + def test_create_console_nport_exceed(self, mock_get): + mock_handler = mock.MagicMock() + mock_handler.side_effect = ( + exception.ImageSerialPortNumberExceedFlavorValue()) + self.controller.handlers['serial'] = mock_handler + + body = {'remote_console': {'protocol': 'serial', 'type': 'serial'}} + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + self.req, fakes.FAKE_UUID, body=body) + + @mock.patch.object(compute_api.API, 'get', return_value='fake_instance') + def test_create_console_socket_exhausted(self, mock_get): + mock_handler = mock.MagicMock() + mock_handler.side_effect = ( + exception.SocketPortRangeExhaustedException(host='127.0.0.1')) + self.controller.handlers['serial'] = mock_handler + + body = {'remote_console': {'protocol': 'serial', 'type': 'serial'}} + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + self.req, fakes.FAKE_UUID, body=body) + + class ConsolesExtensionTestV2(ConsolesExtensionTestV21): controller_class = console_v2.ConsolesController validation_error = webob.exc.HTTPBadRequest diff --git a/nova/tests/unit/api/openstack/compute/test_versions.py b/nova/tests/unit/api/openstack/compute/test_versions.py index 2b1e7a99f5..d54e723c5b 100644 --- a/nova/tests/unit/api/openstack/compute/test_versions.py +++ b/nova/tests/unit/api/openstack/compute/test_versions.py @@ -65,7 +65,7 @@ EXP_VERSIONS = { "v2.1": { "id": "v2.1", "status": "CURRENT", - "version": "2.5", + "version": "2.6", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z", "links": [ @@ -114,7 +114,7 @@ class VersionsTestV20(test.NoDBTestCase): { "id": "v2.1", "status": "CURRENT", - "version": "2.5", + "version": "2.6", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z", "links": [