Consolidate the APIs for getting consoles
A new API is added with microversion 2.6:
POST /servers/<uuid>/remote-consoles
{
"remote_console": {
"protocol": ["vnc"|"rdp"|"serial"|"spice"],
"type": ["novnc"|"xpvnc"|"rdp-html5"|"spice-html5"|"serial"]
}
}
which supports all protocols and types for remote consoles.
Implements: blueprint consolidate-console-api
APIImpact
Change-Id: I175a778cede8fbeee9c47a502ab7a98f6d73c074
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.5",
|
||||
"version": "2.6",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"remote_console": {
|
||||
"protocol": "vnc",
|
||||
"type": "novnc"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"remote_console": {
|
||||
"protocol": "vnc",
|
||||
"type": "novnc",
|
||||
"url": "http://example.com:6080/vnc_auto.html?token=b60bcfc3-5fd4-4d21-986c-e83379107819"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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/<uuid>/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.
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.5",
|
||||
"version": "2.6",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"remote_console": {
|
||||
"protocol": "vnc",
|
||||
"type": "novnc"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"remote_console": {
|
||||
"protocol": "vnc",
|
||||
"type": "novnc",
|
||||
"url": "%(url)s"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user