From e638a8f2ecdb0035b25aca6e8867c732296c0782 Mon Sep 17 00:00:00 2001 From: Alessandro Pilotti Date: Fri, 23 Aug 2013 17:55:14 +0300 Subject: Adds RDP console support Implements: blueprint hyper-v-rdp-console Nova currently supports VNC and SPICE remote console protocols. This commit adds support for the RDP protocol in a similar way. Change-Id: I2c219d4a200122c6d6cfcbd8e074dca0f6fea598 --- .../os-consoles/get-rdp-console-post-req.json | 5 + .../os-consoles/get-rdp-console-post-req.xml | 2 + .../os-consoles/get-rdp-console-post-resp.json | 6 + .../os-consoles/get-rdp-console-post-resp.xml | 5 + .../get-rdp-console-post-req.json | 5 + .../get-rdp-console-post-resp.json | 6 + etc/nova/nova.conf.sample | 14 +++ nova/api/openstack/compute/contrib/consoles.py | 28 ++++- .../compute/plugins/v3/remote_consoles.py | 29 +++++ nova/compute/api.py | 20 +++ nova/compute/cells_api.py | 16 +++ nova/compute/manager.py | 44 ++++++- nova/compute/rpcapi.py | 8 ++ nova/rdp/__init__.py | 31 +++++ .../api/openstack/compute/contrib/test_consoles.py | 92 ++++++++++++++ .../compute/plugins/v3/test_remote_consoles.py | 109 +++++++++++++++++ nova/tests/compute/test_compute.py | 134 +++++++++++++++++++++ nova/tests/compute/test_rpcapi.py | 5 + nova/tests/fake_policy.py | 1 + .../os-consoles/get-rdp-console-post-req.json.tpl | 5 + .../os-consoles/get-rdp-console-post-req.xml.tpl | 4 + .../os-consoles/get-rdp-console-post-resp.json.tpl | 6 + .../os-consoles/get-rdp-console-post-resp.xml.tpl | 5 + nova/tests/integrated/test_api_samples.py | 12 ++ .../get-rdp-console-post-req.json.tpl | 5 + .../get-rdp-console-post-resp.json.tpl | 6 + nova/tests/integrated/v3/test_remote_consoles.py | 12 ++ nova/tests/virt/test_virt_drivers.py | 8 ++ nova/virt/driver.py | 8 ++ nova/virt/fake.py | 5 + 30 files changed, 633 insertions(+), 3 deletions(-) create mode 100644 doc/api_samples/os-consoles/get-rdp-console-post-req.json create mode 100644 doc/api_samples/os-consoles/get-rdp-console-post-req.xml create mode 100644 doc/api_samples/os-consoles/get-rdp-console-post-resp.json create mode 100644 doc/api_samples/os-consoles/get-rdp-console-post-resp.xml create mode 100644 doc/v3/api_samples/os-remote-consoles/get-rdp-console-post-req.json create mode 100644 doc/v3/api_samples/os-remote-consoles/get-rdp-console-post-resp.json create mode 100644 nova/rdp/__init__.py create mode 100644 nova/tests/integrated/api_samples/os-consoles/get-rdp-console-post-req.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-consoles/get-rdp-console-post-req.xml.tpl create mode 100644 nova/tests/integrated/api_samples/os-consoles/get-rdp-console-post-resp.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-consoles/get-rdp-console-post-resp.xml.tpl create mode 100644 nova/tests/integrated/v3/api_samples/os-remote-consoles/get-rdp-console-post-req.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/os-remote-consoles/get-rdp-console-post-resp.json.tpl diff --git a/doc/api_samples/os-consoles/get-rdp-console-post-req.json b/doc/api_samples/os-consoles/get-rdp-console-post-req.json new file mode 100644 index 0000000000..00956b90e4 --- /dev/null +++ b/doc/api_samples/os-consoles/get-rdp-console-post-req.json @@ -0,0 +1,5 @@ +{ + "os-getRDPConsole": { + "type": "rdp-html5" + } +} diff --git a/doc/api_samples/os-consoles/get-rdp-console-post-req.xml b/doc/api_samples/os-consoles/get-rdp-console-post-req.xml new file mode 100644 index 0000000000..16cf28832d --- /dev/null +++ b/doc/api_samples/os-consoles/get-rdp-console-post-req.xml @@ -0,0 +1,2 @@ + + diff --git a/doc/api_samples/os-consoles/get-rdp-console-post-resp.json b/doc/api_samples/os-consoles/get-rdp-console-post-resp.json new file mode 100644 index 0000000000..583d9c1e40 --- /dev/null +++ b/doc/api_samples/os-consoles/get-rdp-console-post-resp.json @@ -0,0 +1,6 @@ +{ + "console": { + "type": "rdp-html5", + "url": "http://example.com:6083/?token=f9906a48-b71e-4f18-baca-c987da3ebdb3&title=dafa(75ecef58-3b8e-4659-ab3b-5501454188e9)" + } +} diff --git a/doc/api_samples/os-consoles/get-rdp-console-post-resp.xml b/doc/api_samples/os-consoles/get-rdp-console-post-resp.xml new file mode 100644 index 0000000000..6c45d6e269 --- /dev/null +++ b/doc/api_samples/os-consoles/get-rdp-console-post-resp.xml @@ -0,0 +1,5 @@ + + + rdp-html5 + http://example.com:6083/?token=f9906a48-b71e-4f18-baca-c987da3ebdb3 + diff --git a/doc/v3/api_samples/os-remote-consoles/get-rdp-console-post-req.json b/doc/v3/api_samples/os-remote-consoles/get-rdp-console-post-req.json new file mode 100644 index 0000000000..075d3b28a8 --- /dev/null +++ b/doc/v3/api_samples/os-remote-consoles/get-rdp-console-post-req.json @@ -0,0 +1,5 @@ +{ + "get_rdp_console": { + "type": "rdp-html5" + } +} diff --git a/doc/v3/api_samples/os-remote-consoles/get-rdp-console-post-resp.json b/doc/v3/api_samples/os-remote-consoles/get-rdp-console-post-resp.json new file mode 100644 index 0000000000..09f3ca3d8c --- /dev/null +++ b/doc/v3/api_samples/os-remote-consoles/get-rdp-console-post-resp.json @@ -0,0 +1,6 @@ +{ + "console": { + "type": "rdp-html5", + "url": "http://127.0.0.1:6083/?token=191996c3-7b0f-42f3-95a7-f1839f2da6ed" + } +} diff --git a/etc/nova/nova.conf.sample b/etc/nova/nova.conf.sample index 5aa2a5a8fd..7834040b6c 100644 --- a/etc/nova/nova.conf.sample +++ b/etc/nova/nova.conf.sample @@ -3033,6 +3033,20 @@ #extensions_whitelist= +[rdp] + +# +# Options defined in nova.rdp +# + +# Location of RDP html5 console proxy, in the form +# "http://127.0.0.1:6083/" (string value) +#html5_proxy_base_url=http://127.0.0.1:6083/ + +# Enable RDP related features (boolean value) +#enabled=false + + [remote_debug] # diff --git a/nova/api/openstack/compute/contrib/consoles.py b/nova/api/openstack/compute/contrib/consoles.py index 07bab0be48..54ebc9a683 100644 --- a/nova/api/openstack/compute/contrib/consoles.py +++ b/nova/api/openstack/compute/contrib/consoles.py @@ -75,12 +75,38 @@ class ConsolesController(wsgi.Controller): return {'console': {'type': console_type, 'url': output['url']}} + @wsgi.action('os-getRDPConsole') + def get_rdp_console(self, req, id, body): + """Get text console output.""" + context = req.environ['nova.context'] + authorize(context) + + # If type is not supplied or unknown, get_rdp_console below will cope + console_type = body['os-getRDPConsole'].get('type') + + try: + instance = self.compute_api.get(context, id, want_objects=True) + output = self.compute_api.get_rdp_console(context, + instance, + console_type) + 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 NotImplementedError: + msg = _("Unable to get rdp console, functionality not implemented") + raise webob.exc.HTTPNotImplemented(explanation=msg) + + return {'console': {'type': console_type, 'url': output['url']}} + def get_actions(self): """Return the actions the extension adds, as required by contract.""" actions = [extensions.ActionExtension("servers", "os-getVNCConsole", self.get_vnc_console), extensions.ActionExtension("servers", "os-getSPICEConsole", - self.get_spice_console)] + self.get_spice_console), + extensions.ActionExtension("servers", "os-getRDPConsole", + self.get_rdp_console)] return actions diff --git a/nova/api/openstack/compute/plugins/v3/remote_consoles.py b/nova/api/openstack/compute/plugins/v3/remote_consoles.py index e78fa0ad0c..726dc96344 100644 --- a/nova/api/openstack/compute/plugins/v3/remote_consoles.py +++ b/nova/api/openstack/compute/plugins/v3/remote_consoles.py @@ -85,6 +85,35 @@ class RemoteConsolesController(wsgi.Controller): return {'console': {'type': console_type, 'url': output['url']}} + @extensions.expected_errors((400, 404, 409, 501)) + @wsgi.action('get_rdp_console') + def get_rdp_console(self, req, id, body): + """Get text console output.""" + context = req.environ['nova.context'] + authorize(context) + + # If type is not supplied or unknown, get_rdp_console below will cope + console_type = body['get_rdp_console'].get('type') + + try: + instance = self.compute_api.get(context, id, want_objects=True) + output = self.compute_api.get_rdp_console(context, + instance, + console_type) + except exception.ConsoleTypeInvalid as e: + raise webob.exc.HTTPBadRequest(explanation=e.format_message()) + except exception.ConsoleTypeUnavailable as e: + raise webob.exc.HTTPBadRequest(explanation=e.format_message()) + 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 NotImplementedError: + msg = _("Unable to get rdp console, functionality not implemented") + raise webob.exc.HTTPNotImplemented(explanation=msg) + + return {'console': {'type': console_type, 'url': output['url']}} + class RemoteConsoles(extensions.V3APIExtensionBase): """Interactive Console support.""" diff --git a/nova/compute/api.py b/nova/compute/api.py index 20b00bfa1e..2e7af4ee59 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -2634,6 +2634,26 @@ class API(base.Base): instance=instance, console_type=console_type) return connect_info + @wrap_check_policy + @check_instance_host + def get_rdp_console(self, context, instance, console_type): + """Get a url to an instance Console.""" + connect_info = self.compute_rpcapi.get_rdp_console(context, + instance=instance, console_type=console_type) + self.consoleauth_rpcapi.authorize_console(context, + connect_info['token'], console_type, + connect_info['host'], connect_info['port'], + connect_info['internal_access_path'], instance['uuid']) + + return {'url': connect_info['access_url']} + + @check_instance_host + def get_rdp_connect_info(self, context, instance, console_type): + """Used in a child cell to get console info.""" + connect_info = self.compute_rpcapi.get_rdp_console(context, + instance=instance, console_type=console_type) + return connect_info + @wrap_check_policy @check_instance_host def get_console_output(self, context, instance, tail_length=None): diff --git a/nova/compute/cells_api.py b/nova/compute/cells_api.py index 5543b5bf9d..0f31c24286 100644 --- a/nova/compute/cells_api.py +++ b/nova/compute/cells_api.py @@ -384,6 +384,22 @@ class ComputeCellsAPI(compute_api.API): instance['uuid']) return {'url': connect_info['access_url']} + @wrap_check_policy + @check_instance_cell + def get_rdp_console(self, context, instance, console_type): + """Get a url to a RDP Console.""" + if not instance['host']: + raise exception.InstanceNotReady(instance_id=instance['uuid']) + + connect_info = self._call_to_cells(context, instance, + 'get_rdp_connect_info', console_type) + + self.consoleauth_rpcapi.authorize_console(context, + connect_info['token'], console_type, connect_info['host'], + connect_info['port'], connect_info['internal_access_path'], + instance['uuid']) + return {'url': connect_info['access_url']} + @check_instance_cell def get_console_output(self, context, instance, *args, **kwargs): """Get console output for an an instance.""" diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 24ba25c77e..97b92e9413 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -207,6 +207,8 @@ CONF.import_opt('enabled', 'nova.spice', group='spice') CONF.import_opt('enable', 'nova.cells.opts', group='cells') CONF.import_opt('image_cache_subdirectory_name', 'nova.virt.imagecache') CONF.import_opt('image_cache_manager_interval', 'nova.virt.imagecache') +CONF.import_opt('enabled', 'nova.rdp', group='rdp') +CONF.import_opt('html5_proxy_base_url', 'nova.rdp', group='rdp') LOG = logging.getLogger(__name__) @@ -409,7 +411,7 @@ class ComputeVirtAPI(virtapi.VirtAPI): class ComputeManager(manager.Manager): """Manages the running instances from creation to destruction.""" - target = messaging.Target(version='3.9') + target = messaging.Target(version='3.10') def __init__(self, compute_driver=None, *args, **kwargs): """Load configuration options and connect to the hypervisor.""" @@ -3760,6 +3762,42 @@ class ComputeManager(manager.Manager): return connect_info + @object_compat + @messaging.expected_exceptions(exception.ConsoleTypeInvalid, + exception.InstanceNotReady, + exception.InstanceNotFound, + exception.ConsoleTypeUnavailable, + NotImplementedError) + @wrap_exception() + @wrap_instance_fault + def get_rdp_console(self, context, console_type, instance): + """Return connection information for a RDP console.""" + context = context.elevated() + LOG.debug(_("Getting RDP console"), instance=instance) + token = str(uuid.uuid4()) + + if not CONF.rdp.enabled: + raise exception.ConsoleTypeInvalid(console_type=console_type) + + if console_type == 'rdp-html5': + access_url = '%s?token=%s' % (CONF.rdp.html5_proxy_base_url, + token) + else: + raise exception.ConsoleTypeInvalid(console_type=console_type) + + try: + # Retrieve connect info from driver, and then decorate with our + # access info token + connect_info = self.driver.get_rdp_console(context, instance) + connect_info['token'] = token + connect_info['access_url'] = access_url + except exception.InstanceNotFound: + if instance['vm_state'] != vm_states.BUILDING: + raise + raise exception.InstanceNotReady(instance_id=instance['uuid']) + + return connect_info + @messaging.expected_exceptions(exception.ConsoleTypeInvalid, exception.InstanceNotReady, exception.InstanceNotFound) @@ -3769,6 +3807,8 @@ class ComputeManager(manager.Manager): def validate_console_port(self, ctxt, instance, port, console_type): if console_type == "spice-html5": console_info = self.driver.get_spice_console(ctxt, instance) + elif console_type == "rdp-html5": + console_info = self.driver.get_rdp_console(ctxt, instance) else: console_info = self.driver.get_vnc_console(ctxt, instance) @@ -4345,7 +4385,7 @@ class ComputeManager(manager.Manager): "This error can be safely ignored."), instance=instance_ref) - if CONF.vnc_enabled or CONF.spice.enabled: + if CONF.vnc_enabled or CONF.spice.enabled or CONF.rdp.enabled: if CONF.cells.enable: self.cells_rpcapi.consoleauth_delete_tokens(ctxt, instance_ref['uuid']) diff --git a/nova/compute/rpcapi.py b/nova/compute/rpcapi.py index 4140a72297..2cdd409a98 100644 --- a/nova/compute/rpcapi.py +++ b/nova/compute/rpcapi.py @@ -223,6 +223,7 @@ class ComputeAPI(object): 3.7 - Update change_instance_metadata() to take an instance object 3.8 - Update set_admin_password() to take an instance object 3.9 - Update rescue_instance() to take an instance object + 3.10 - Added get_rdp_console method ''' VERSION_ALIASES = { @@ -450,6 +451,13 @@ class ComputeAPI(object): return cctxt.call(ctxt, 'get_spice_console', instance=instance, console_type=console_type) + def get_rdp_console(self, ctxt, instance, console_type): + version = '3.10' + cctxt = self.client.prepare(server=_compute_host(None, instance), + version=version) + return cctxt.call(ctxt, 'get_rdp_console', + instance=instance, console_type=console_type) + def validate_console_port(self, ctxt, instance, port, console_type): if self.client.can_send_version('3.3'): version = '3.3' diff --git a/nova/rdp/__init__.py b/nova/rdp/__init__.py new file mode 100644 index 0000000000..ef30ba184b --- /dev/null +++ b/nova/rdp/__init__.py @@ -0,0 +1,31 @@ +# Copyright 2014 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module for RDP Proxying.""" + +from oslo.config import cfg + + +rdp_opts = [ + cfg.StrOpt('html5_proxy_base_url', + default='http://127.0.0.1:6083/', + help='Location of RDP html5 console proxy, in the form ' + '"http://127.0.0.1:6083/"'), + cfg.BoolOpt('enabled', + default=False, + help='Enable RDP related features'), + ] + +CONF = cfg.CONF +CONF.register_opts(rdp_opts, group='rdp') diff --git a/nova/tests/api/openstack/compute/contrib/test_consoles.py b/nova/tests/api/openstack/compute/contrib/test_consoles.py index 8fd6e6abb6..cafed85f22 100644 --- a/nova/tests/api/openstack/compute/contrib/test_consoles.py +++ b/nova/tests/api/openstack/compute/contrib/test_consoles.py @@ -30,6 +30,10 @@ def fake_get_spice_console(self, _context, _instance, _console_type): return {'url': 'http://fake'} +def fake_get_rdp_console(self, _context, _instance, _console_type): + return {'url': 'http://fake'} + + def fake_get_vnc_console_invalid_type(self, _context, _instance, _console_type): raise exception.ConsoleTypeInvalid(console_type=_console_type) @@ -40,6 +44,11 @@ def fake_get_spice_console_invalid_type(self, _context, raise exception.ConsoleTypeInvalid(console_type=_console_type) +def fake_get_rdp_console_invalid_type(self, _context, + _instance, _console_type): + raise exception.ConsoleTypeInvalid(console_type=_console_type) + + def fake_get_vnc_console_not_ready(self, _context, instance, _console_type): raise exception.InstanceNotReady(instance_id=instance["uuid"]) @@ -48,6 +57,10 @@ def fake_get_spice_console_not_ready(self, _context, instance, _console_type): raise exception.InstanceNotReady(instance_id=instance["uuid"]) +def fake_get_rdp_console_not_ready(self, _context, instance, _console_type): + raise exception.InstanceNotReady(instance_id=instance["uuid"]) + + def fake_get_vnc_console_not_found(self, _context, instance, _console_type): raise exception.InstanceNotFound(instance_id=instance["uuid"]) @@ -56,6 +69,10 @@ def fake_get_spice_console_not_found(self, _context, instance, _console_type): raise exception.InstanceNotFound(instance_id=instance["uuid"]) +def fake_get_rdp_console_not_found(self, _context, instance, _console_type): + raise exception.InstanceNotFound(instance_id=instance["uuid"]) + + def fake_get(self, context, instance_uuid, want_objects=False): return {'uuid': instance_uuid} @@ -72,6 +89,8 @@ class ConsolesExtensionTest(test.NoDBTestCase): fake_get_vnc_console) self.stubs.Set(compute_api.API, 'get_spice_console', fake_get_spice_console) + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console) self.stubs.Set(compute_api.API, 'get', fake_get) self.flags( osapi_compute_extension=[ @@ -237,3 +256,76 @@ class ConsolesExtensionTest(test.NoDBTestCase): res = req.get_response(self.app) self.assertEqual(res.status_int, 400) + + def test_get_rdp_console(self): + body = {'os-getRDPConsole': {'type': 'rdp-html5'}} + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + output = jsonutils.loads(res.body) + self.assertEqual(res.status_int, 200) + self.assertEqual(output, + {u'console': {u'url': u'http://fake', u'type': u'rdp-html5'}}) + + def test_get_rdp_console_not_ready(self): + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console_not_ready) + body = {'os-getRDPConsole': {'type': 'rdp-html5'}} + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + output = jsonutils.loads(res.body) + self.assertEqual(res.status_int, 409) + + def test_get_rdp_console_no_type(self): + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console_invalid_type) + body = {'os-getRDPConsole': {}} + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + + def test_get_rdp_console_no_instance(self): + self.stubs.Set(compute_api.API, 'get', fake_get_not_found) + body = {'os-getRDPConsole': {'type': 'rdp-html5'}} + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 404) + + def test_get_rdp_console_no_instance_on_console_get(self): + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console_not_found) + body = {'os-getRDPConsole': {'type': 'rdp-html5'}} + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 404) + + def test_get_rdp_console_invalid_type(self): + body = {'os-getRDPConsole': {'type': 'invalid'}} + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console_invalid_type) + req = webob.Request.blank('/v2/fake/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_remote_consoles.py b/nova/tests/api/openstack/compute/plugins/v3/test_remote_consoles.py index f4ee188aea..1af57e0558 100644 --- a/nova/tests/api/openstack/compute/plugins/v3/test_remote_consoles.py +++ b/nova/tests/api/openstack/compute/plugins/v3/test_remote_consoles.py @@ -30,6 +30,10 @@ def fake_get_spice_console(self, _context, _instance, _console_type): return {'url': 'http://fake'} +def fake_get_rdp_console(self, _context, _instance, _console_type): + return {'url': 'http://fake'} + + def fake_get_vnc_console_invalid_type(self, _context, _instance, _console_type): raise exception.ConsoleTypeInvalid(console_type=_console_type) @@ -40,6 +44,11 @@ def fake_get_spice_console_invalid_type(self, _context, raise exception.ConsoleTypeInvalid(console_type=_console_type) +def fake_get_rdp_console_invalid_type(self, _context, + _instance, _console_type): + raise exception.ConsoleTypeInvalid(console_type=_console_type) + + def fake_get_vnc_console_type_unavailable(self, _context, _instance, _console_type): raise exception.ConsoleTypeUnavailable(console_type=_console_type) @@ -50,6 +59,11 @@ def fake_get_spice_console_type_unavailable(self, _context, raise exception.ConsoleTypeUnavailable(console_type=_console_type) +def fake_get_rdp_console_type_unavailable(self, _context, + _instance, _console_type): + raise exception.ConsoleTypeUnavailable(console_type=_console_type) + + def fake_get_vnc_console_not_ready(self, _context, instance, _console_type): raise exception.InstanceNotReady(instance_id=instance["uuid"]) @@ -58,6 +72,10 @@ def fake_get_spice_console_not_ready(self, _context, instance, _console_type): raise exception.InstanceNotReady(instance_id=instance["uuid"]) +def fake_get_rdp_console_not_ready(self, _context, instance, _console_type): + raise exception.InstanceNotReady(instance_id=instance["uuid"]) + + def fake_get_vnc_console_not_found(self, _context, instance, _console_type): raise exception.InstanceNotFound(instance_id=instance["uuid"]) @@ -66,6 +84,10 @@ def fake_get_spice_console_not_found(self, _context, instance, _console_type): raise exception.InstanceNotFound(instance_id=instance["uuid"]) +def fake_get_rdp_console_not_found(self, _context, instance, _console_type): + raise exception.InstanceNotFound(instance_id=instance["uuid"]) + + def fake_get(self, context, instance_uuid, want_objects=False): return {'uuid': instance_uuid} @@ -82,6 +104,8 @@ class ConsolesExtensionTest(test.NoDBTestCase): fake_get_vnc_console) self.stubs.Set(compute_api.API, 'get_spice_console', fake_get_spice_console) + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console) self.stubs.Set(compute_api.API, 'get', fake_get) self.app = fakes.wsgi_app_v3(init_only=('servers', 'os-remote-consoles')) @@ -268,3 +292,88 @@ class ConsolesExtensionTest(test.NoDBTestCase): res = req.get_response(self.app) self.assertEqual(400, res.status_int) + + def test_get_rdp_console(self): + body = {'get_rdp_console': {'type': 'rdp-html5'}} + req = webob.Request.blank('/v3/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + output = jsonutils.loads(res.body) + self.assertEqual(res.status_int, 200) + self.assertEqual(output, + {u'console': {u'url': u'http://fake', u'type': u'rdp-html5'}}) + + def test_get_rdp_console_not_ready(self): + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console_not_ready) + body = {'get_rdp_console': {'type': 'rdp-html5'}} + req = webob.Request.blank('/v3/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + output = jsonutils.loads(res.body) + self.assertEqual(res.status_int, 409) + + def test_get_rdp_console_no_type(self): + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console_invalid_type) + body = {'get_rdp_console': {}} + req = webob.Request.blank('/v3/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + + def test_get_rdp_console_no_instance(self): + self.stubs.Set(compute_api.API, 'get', fake_get_not_found) + body = {'get_rdp_console': {'type': 'rdp-html5'}} + req = webob.Request.blank('/v3/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 404) + + def test_get_rdp_console_no_instance_on_console_get(self): + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console_not_found) + body = {'get_rdp_console': {'type': 'rdp-html5'}} + req = webob.Request.blank('/v3/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 404) + + def test_get_rdp_console_invalid_type(self): + body = {'get_rdp_console': {'type': 'invalid'}} + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console_invalid_type) + req = webob.Request.blank('/v3/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(res.status_int, 400) + + def test_get_rdp_console_type_unavailable(self): + body = {'get_rdp_console': {'type': 'unavailable'}} + self.stubs.Set(compute_api.API, 'get_rdp_console', + fake_get_rdp_console_type_unavailable) + req = webob.Request.blank('/v3/servers/1/action') + req.method = "POST" + req.body = jsonutils.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(self.app) + self.assertEqual(400, res.status_int) diff --git a/nova/tests/compute/test_compute.py b/nova/tests/compute/test_compute.py index bf58634ef9..950b8c2c4c 100644 --- a/nova/tests/compute/test_compute.py +++ b/nova/tests/compute/test_compute.py @@ -2746,6 +2746,20 @@ class ComputeTestCase(BaseTestCase): context=self.context, instance=instance, port="5900", console_type="spice-html5")) + def test_validate_console_port_rdp(self): + self.flags(enabled=True, group='rdp') + instance = self._create_fake_instance_obj() + + def fake_driver_get_console(*args, **kwargs): + return {'host': "fake_host", 'port': "5900", + 'internal_access_path': None} + self.stubs.Set(self.compute.driver, "get_rdp_console", + fake_driver_get_console) + + self.assertTrue(self.compute.validate_console_port( + context=self.context, instance=instance, port="5900", + console_type="rdp-html5")) + def test_validate_console_port_wrong_port(self): self.flags(vnc_enabled=True) self.flags(enabled=True, group='spice') @@ -2902,6 +2916,67 @@ class ComputeTestCase(BaseTestCase): self.compute.terminate_instance(self.context, instance, [], []) + def test_rdphtml5_rdp_console(self): + # Make sure we can a rdp console for an instance. + self.flags(vnc_enabled=False) + self.flags(enabled=True, group='rdp') + + instance = self._create_fake_instance_obj() + self.compute.run_instance(self.context, + jsonutils.to_primitive(instance), {}, {}, [], None, + None, True, None, False) + + # Try with the full instance + console = self.compute.get_rdp_console(self.context, 'rdp-html5', + instance=instance) + self.assertTrue(console) + + self.compute.terminate_instance(self.context, instance, [], []) + + def test_invalid_rdp_console_type(self): + # Raise useful error if console type is an unrecognised string + self.flags(vnc_enabled=False) + self.flags(enabled=True, group='rdp') + + instance = self._create_fake_instance_obj() + self.compute.run_instance(self.context, + jsonutils.to_primitive(instance), {}, {}, [], None, + None, True, None, False) + + self.assertRaises(messaging.ExpectedException, + self.compute.get_rdp_console, + self.context, 'invalid', instance=instance) + + self.compute = utils.ExceptionHelper(self.compute) + + self.assertRaises(exception.ConsoleTypeInvalid, + self.compute.get_rdp_console, + self.context, 'invalid', instance=instance) + + self.compute.terminate_instance(self.context, instance, [], []) + + def test_missing_rdp_console_type(self): + # Raise useful error is console type is None + self.flags(vnc_enabled=False) + self.flags(enabled=True, group='rdp') + + instance = self._create_fake_instance_obj() + self.compute.run_instance(self.context, + jsonutils.to_primitive(instance), {}, {}, [], None, + None, True, None, False) + + self.assertRaises(messaging.ExpectedException, + self.compute.get_rdp_console, + self.context, None, instance=instance) + + self.compute = utils.ExceptionHelper(self.compute) + + self.assertRaises(exception.ConsoleTypeInvalid, + self.compute.get_rdp_console, + self.context, None, instance=instance) + + self.compute.terminate_instance(self.context, instance, [], []) + def test_vnc_console_instance_not_ready(self): self.flags(vnc_enabled=True) self.flags(enabled=False, group='spice') @@ -2938,6 +3013,24 @@ class ComputeTestCase(BaseTestCase): self.compute.get_spice_console, self.context, 'spice-html5', instance=instance) + def test_rdp_console_instance_not_ready(self): + self.flags(vnc_enabled=False) + self.flags(enabled=True, group='rdp') + instance = self._create_fake_instance_obj( + params={'vm_state': vm_states.BUILDING}) + + def fake_driver_get_console(*args, **kwargs): + raise exception.InstanceNotFound(instance_id=instance['uuid']) + + self.stubs.Set(self.compute.driver, "get_rdp_console", + fake_driver_get_console) + + self.compute = utils.ExceptionHelper(self.compute) + + self.assertRaises(exception.InstanceNotReady, + self.compute.get_rdp_console, self.context, 'rdp-html5', + instance=instance) + def test_diagnostics(self): # Make sure we can get diagnostics for an instance. expected_diagnostic = {'cpu0_time': 17300000000, @@ -8039,6 +8132,47 @@ class ComputeAPITestCase(BaseTestCase): db.instance_destroy(self.context, instance['uuid']) + def test_rdp_console(self): + # Make sure we can a rdp console for an instance. + + fake_instance = {'uuid': 'fake_uuid', + 'host': 'fake_compute_host'} + fake_console_type = "rdp-html5" + fake_connect_info = {'token': 'fake_token', + 'console_type': fake_console_type, + 'host': 'fake_console_host', + 'port': 'fake_console_port', + 'internal_access_path': 'fake_access_path', + 'instance_uuid': fake_instance['uuid'], + 'access_url': 'fake_console_url'} + + rpcapi = compute_rpcapi.ComputeAPI + self.mox.StubOutWithMock(rpcapi, 'get_rdp_console') + rpcapi.get_rdp_console( + self.context, instance=fake_instance, + console_type=fake_console_type).AndReturn(fake_connect_info) + + self.mox.StubOutWithMock(self.compute_api.consoleauth_rpcapi, + 'authorize_console') + self.compute_api.consoleauth_rpcapi.authorize_console( + self.context, 'fake_token', fake_console_type, 'fake_console_host', + 'fake_console_port', 'fake_access_path', 'fake_uuid') + + self.mox.ReplayAll() + + console = self.compute_api.get_rdp_console(self.context, + fake_instance, fake_console_type) + self.assertEqual(console, {'url': 'fake_console_url'}) + + def test_get_rdp_console_no_host(self): + instance = self._create_fake_instance(params={'host': ''}) + + self.assertRaises(exception.InstanceNotReady, + self.compute_api.get_rdp_console, + self.context, instance, 'rdp') + + db.instance_destroy(self.context, instance['uuid']) + def test_console_output(self): fake_instance = {'uuid': 'fake_uuid', 'host': 'fake_compute_host'} diff --git a/nova/tests/compute/test_rpcapi.py b/nova/tests/compute/test_rpcapi.py index 0b56e5d06b..e48810eaa8 100644 --- a/nova/tests/compute/test_rpcapi.py +++ b/nova/tests/compute/test_rpcapi.py @@ -305,6 +305,11 @@ class ComputeRpcAPITestCase(test.TestCase): instance=self.fake_instance, console_type='type', version='2.24') + def test_get_rdp_console(self): + self._test_compute_api('get_rdp_console', 'call', + instance=self.fake_instance, console_type='type', + version='3.10') + def test_validate_console_port(self): self._test_compute_api('validate_console_port', 'call', instance=self.fake_instance, port="5900", diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index 8a18826608..efaef59235 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -46,6 +46,7 @@ policy_data = """ "compute:get_vnc_console": "", "compute:get_spice_console": "", + "compute:get_rdp_console": "", "compute:get_console_output": "", "compute:associate_floating_ip": "", diff --git a/nova/tests/integrated/api_samples/os-consoles/get-rdp-console-post-req.json.tpl b/nova/tests/integrated/api_samples/os-consoles/get-rdp-console-post-req.json.tpl new file mode 100644 index 0000000000..00956b90e4 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-consoles/get-rdp-console-post-req.json.tpl @@ -0,0 +1,5 @@ +{ + "os-getRDPConsole": { + "type": "rdp-html5" + } +} diff --git a/nova/tests/integrated/api_samples/os-consoles/get-rdp-console-post-req.xml.tpl b/nova/tests/integrated/api_samples/os-consoles/get-rdp-console-post-req.xml.tpl new file mode 100644 index 0000000000..b761d78b67 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-consoles/get-rdp-console-post-req.xml.tpl @@ -0,0 +1,4 @@ + + + rdp-html5 + diff --git a/nova/tests/integrated/api_samples/os-consoles/get-rdp-console-post-resp.json.tpl b/nova/tests/integrated/api_samples/os-consoles/get-rdp-console-post-resp.json.tpl new file mode 100644 index 0000000000..b8272ca5c0 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-consoles/get-rdp-console-post-resp.json.tpl @@ -0,0 +1,6 @@ +{ + "console": { + "type": "rdp-html5", + "url":"%(url)s" + } +} diff --git a/nova/tests/integrated/api_samples/os-consoles/get-rdp-console-post-resp.xml.tpl b/nova/tests/integrated/api_samples/os-consoles/get-rdp-console-post-resp.xml.tpl new file mode 100644 index 0000000000..24fc3cd848 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-consoles/get-rdp-console-post-resp.xml.tpl @@ -0,0 +1,5 @@ + + + rdp-html5 + %(url)s + diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py index a81ab20e72..fab6009346 100644 --- a/nova/tests/integrated/test_api_samples.py +++ b/nova/tests/integrated/test_api_samples.py @@ -1952,6 +1952,7 @@ class ConsolesSampleJsonTests(ServersSampleBase): super(ConsolesSampleJsonTests, self).setUp() self.flags(vnc_enabled=True) self.flags(enabled=True, group='spice') + self.flags(enabled=True, group='rdp') def test_get_vnc_console(self): uuid = self._post_server() @@ -1974,6 +1975,17 @@ class ConsolesSampleJsonTests(ServersSampleBase): self._verify_response('get-spice-console-post-resp', subs, response, 200) + def test_get_rdp_console(self): + uuid = self._post_server() + response = self._do_post('servers/%s/action' % uuid, + 'get-rdp-console-post-req', + {'action': 'os-getRDPConsole'}) + subs = self._get_regexes() + subs["url"] = \ + "((https?):((//)|(\\\\))+([\w\d:#@%/;$()~_?\+-=\\\.&](#!)?)*)" + self._verify_response('get-rdp-console-post-resp', subs, + response, 200) + class ConsolesSampleXmlTests(ConsolesSampleJsonTests): ctype = 'xml' diff --git a/nova/tests/integrated/v3/api_samples/os-remote-consoles/get-rdp-console-post-req.json.tpl b/nova/tests/integrated/v3/api_samples/os-remote-consoles/get-rdp-console-post-req.json.tpl new file mode 100644 index 0000000000..075d3b28a8 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-remote-consoles/get-rdp-console-post-req.json.tpl @@ -0,0 +1,5 @@ +{ + "get_rdp_console": { + "type": "rdp-html5" + } +} diff --git a/nova/tests/integrated/v3/api_samples/os-remote-consoles/get-rdp-console-post-resp.json.tpl b/nova/tests/integrated/v3/api_samples/os-remote-consoles/get-rdp-console-post-resp.json.tpl new file mode 100644 index 0000000000..c3955d6ac0 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-remote-consoles/get-rdp-console-post-resp.json.tpl @@ -0,0 +1,6 @@ +{ + "console": { + "type": "rdp-html5", + "url": "http://127.0.0.1:6083/?token=%(uuid)s" + } +} diff --git a/nova/tests/integrated/v3/test_remote_consoles.py b/nova/tests/integrated/v3/test_remote_consoles.py index 3d61b72df9..b7f1cef8c9 100644 --- a/nova/tests/integrated/v3/test_remote_consoles.py +++ b/nova/tests/integrated/v3/test_remote_consoles.py @@ -23,6 +23,7 @@ class ConsolesSampleJsonTests(test_servers.ServersSampleBase): super(ConsolesSampleJsonTests, self).setUp() self.flags(vnc_enabled=True) self.flags(enabled=True, group='spice') + self.flags(enabled=True, group='rdp') def test_get_vnc_console(self): uuid = self._post_server() @@ -44,3 +45,14 @@ class ConsolesSampleJsonTests(test_servers.ServersSampleBase): "((https?):((//)|(\\\\))+([\w\d:#@%/;$()~_?\+-=\\\.&](#!)?)*)" self._verify_response('get-spice-console-post-resp', subs, response, 200) + + def test_get_rdp_console(self): + uuid = self._post_server() + response = self._do_post('servers/%s/action' % uuid, + 'get-rdp-console-post-req', + {'action': 'os-getRDPConsole'}) + subs = self._get_regexes() + subs["url"] = \ + "((https?):((//)|(\\\\))+([\w\d:#@%/;$()~_?\+-=\\\.&](#!)?)*)" + self._verify_response('get-rdp-console-post-resp', subs, + response, 200) diff --git a/nova/tests/virt/test_virt_drivers.py b/nova/tests/virt/test_virt_drivers.py index 563759879c..8e0a2d7230 100644 --- a/nova/tests/virt/test_virt_drivers.py +++ b/nova/tests/virt/test_virt_drivers.py @@ -518,6 +518,14 @@ class _VirtDriverTestCase(_FakeDriverBackendTestCase): self.assertIn('port', spice_console) self.assertIn('tlsPort', spice_console) + @catch_notimplementederror + def test_get_rdp_console(self): + instance_ref, network_info = self._get_running_instance() + rdp_console = self.connection.get_rdp_console(self.ctxt, instance_ref) + self.assertIn('internal_access_path', rdp_console) + self.assertIn('host', rdp_console) + self.assertIn('port', rdp_console) + @catch_notimplementederror def test_get_console_pool_info(self): instance_ref, network_info = self._get_running_instance() diff --git a/nova/virt/driver.py b/nova/virt/driver.py index 381c906d70..8299428697 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -365,6 +365,14 @@ class ComputeDriver(object): """ raise NotImplementedError() + def get_rdp_console(self, context, instance): + """Get connection info for a rdp console. + + :param context: security context + :param instance: nova.objects.instance.Instance + """ + raise NotImplementedError() + def get_diagnostics(self, instance): """Return data about VM diagnostics.""" # TODO(Vek): Need to pass context in for access to auth_token diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 33dd687437..77012c1c36 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -324,6 +324,11 @@ class FakeDriver(driver.ComputeDriver): 'port': 6969, 'tlsPort': 6970} + def get_rdp_console(self, context, instance): + return {'internal_access_path': 'FAKE', + 'host': 'fakerdpconsole.com', + 'port': 6969} + def get_console_pool_info(self, console_type): return {'address': '127.0.0.1', 'username': 'fakeuser', -- cgit v1.2.1