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:
Radoslav Gerganov
2015-01-20 13:23:32 +02:00
parent 6969f270c5
commit 578bafeda0
13 changed files with 292 additions and 6 deletions

View File

@@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
"version": "2.5",
"version": "2.6",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@@ -0,0 +1,6 @@
{
"remote_console": {
"protocol": "vnc",
"type": "novnc"
}
}

View File

@@ -0,0 +1,7 @@
{
"remote_console": {
"protocol": "vnc",
"type": "novnc",
"url": "http://example.com:6080/vnc_auto.html?token=b60bcfc3-5fd4-4d21-986c-e83379107819"
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
"version": "2.5",
"version": "2.6",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@@ -0,0 +1,6 @@
{
"remote_console": {
"protocol": "vnc",
"type": "novnc"
}
}

View File

@@ -0,0 +1,7 @@
{
"remote_console": {
"protocol": "vnc",
"type": "novnc",
"url": "%(url)s"
}
}

View File

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

View File

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

View File

@@ -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": [