diff --git a/doc/api_samples/os-consoles/get-spice-console-post-req.json b/doc/api_samples/os-consoles/get-spice-console-post-req.json
new file mode 100644
index 000000000000..d04f7c7ae9fe
--- /dev/null
+++ b/doc/api_samples/os-consoles/get-spice-console-post-req.json
@@ -0,0 +1,5 @@
+{
+ "os-getSPICEConsole": {
+ "type": "spice-html5"
+ }
+}
diff --git a/doc/api_samples/os-consoles/get-spice-console-post-req.xml b/doc/api_samples/os-consoles/get-spice-console-post-req.xml
new file mode 100644
index 000000000000..59052abea273
--- /dev/null
+++ b/doc/api_samples/os-consoles/get-spice-console-post-req.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/doc/api_samples/os-consoles/get-spice-console-post-resp.json b/doc/api_samples/os-consoles/get-spice-console-post-resp.json
new file mode 100644
index 000000000000..f4999e1babcf
--- /dev/null
+++ b/doc/api_samples/os-consoles/get-spice-console-post-resp.json
@@ -0,0 +1,6 @@
+{
+ "console": {
+ "type": "spice-html5",
+ "url": "http://example.com:6080/spice_auto.html?token=f9906a48-b71e-4f18-baca-c987da3ebdb3&title=dafa(75ecef58-3b8e-4659-ab3b-5501454188e9)"
+ }
+}
diff --git a/doc/api_samples/os-consoles/get-spice-console-post-resp.xml b/doc/api_samples/os-consoles/get-spice-console-post-resp.xml
new file mode 100644
index 000000000000..acba8b1f03a0
--- /dev/null
+++ b/doc/api_samples/os-consoles/get-spice-console-post-resp.xml
@@ -0,0 +1,5 @@
+
+
+ spice-html5
+ http://example.com:6080/spice_auto.html?token=f9906a48-b71e-4f18-baca-c987da3ebdb3
+
diff --git a/nova/api/openstack/compute/contrib/consoles.py b/nova/api/openstack/compute/contrib/consoles.py
index 4f88d033c145..4895a9e7b27a 100644
--- a/nova/api/openstack/compute/contrib/consoles.py
+++ b/nova/api/openstack/compute/contrib/consoles.py
@@ -53,10 +53,33 @@ class ConsolesController(wsgi.Controller):
return {'console': {'type': console_type, 'url': output['url']}}
+ @wsgi.action('os-getSPICEConsole')
+ def get_spice_console(self, req, id, body):
+ """Get text console output."""
+ context = req.environ['nova.context']
+ authorize(context)
+
+ # If type is not supplied or unknown, get_spice_console below will cope
+ console_type = body['os-getSPICEConsole'].get('type')
+
+ try:
+ instance = self.compute_api.get(context, id)
+ output = self.compute_api.get_spice_console(context,
+ instance,
+ console_type)
+ except exception.InstanceNotFound as e:
+ raise webob.exc.HTTPNotFound(explanation=unicode(e))
+ except exception.InstanceNotReady as e:
+ raise webob.exc.HTTPConflict(explanation=unicode(e))
+
+ 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)]
+ self.get_vnc_console),
+ extensions.ActionExtension("servers", "os-getSPICEConsole",
+ self.get_spice_console)]
return actions
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 8ba6b97aa07d..c7ca0640d144 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -2030,6 +2030,29 @@ class API(base.Base):
instance=instance, console_type=console_type)
return connect_info
+ @wrap_check_policy
+ def get_spice_console(self, context, instance, console_type):
+ """Get a url to an instance Console."""
+ if not instance['host']:
+ raise exception.InstanceNotReady(instance_id=instance['uuid'])
+
+ connect_info = self.compute_rpcapi.get_spice_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'])
+
+ return {'url': connect_info['access_url']}
+
+ def get_spice_connect_info(self, context, instance, console_type):
+ """Used in a child cell to get console info."""
+ if not instance['host']:
+ raise exception.InstanceNotReady(instance_id=instance['uuid'])
+ connect_info = self.compute_rpcapi.get_spice_console(context,
+ instance=instance, console_type=console_type)
+ return connect_info
+
@wrap_check_policy
def get_console_output(self, context, instance, tail_length=None):
"""Get console output for an instance."""
diff --git a/nova/compute/cells_api.py b/nova/compute/cells_api.py
index d1d9a11d20ae..d5427a04b027 100644
--- a/nova/compute/cells_api.py
+++ b/nova/compute/cells_api.py
@@ -439,6 +439,21 @@ class ComputeCellsAPI(compute_api.API):
connect_info['port'], connect_info['internal_access_path'])
return {'url': connect_info['access_url']}
+ @wrap_check_policy
+ @validate_cell
+ def get_spice_console(self, context, instance, console_type):
+ """Get a url to a SPICE Console."""
+ if not instance['host']:
+ raise exception.InstanceNotReady(instance_id=instance['uuid'])
+
+ connect_info = self._call_to_cells(context, instance,
+ 'get_spice_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'])
+ return {'url': connect_info['access_url']}
+
@validate_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 3bf8e61efaae..0136f765997c 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -293,7 +293,7 @@ class ComputeVirtAPI(virtapi.VirtAPI):
class ComputeManager(manager.SchedulerDependentManager):
"""Manages the running instances from creation to destruction."""
- RPC_API_VERSION = '2.23'
+ RPC_API_VERSION = '2.24'
def __init__(self, compute_driver=None, *args, **kwargs):
"""Load configuration options and connect to the hypervisor."""
@@ -2387,6 +2387,9 @@ class ComputeManager(manager.SchedulerDependentManager):
LOG.debug(_("Getting vnc console"), instance=instance)
token = str(uuid.uuid4())
+ if not CONF.vnc_enabled:
+ raise exception.ConsoleTypeInvalid(console_type=console_type)
+
if console_type == 'novnc':
# For essex, novncproxy_base_url must include the full path
# including the html file (like http://myhost/vnc_auto.html)
@@ -2404,6 +2407,33 @@ class ComputeManager(manager.SchedulerDependentManager):
return connect_info
+ @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
+ @wrap_instance_fault
+ def get_spice_console(self, context, console_type, instance):
+ """Return connection information for a spice console."""
+ context = context.elevated()
+ LOG.debug(_("Getting spice console"), instance=instance)
+ token = str(uuid.uuid4())
+
+ if not CONF.spice.enabled:
+ raise exception.ConsoleTypeInvalid(console_type=console_type)
+
+ if console_type == 'spice-html5':
+ # For essex, spicehtml5proxy_base_url must include the full path
+ # including the html file (like http://myhost/spice_auto.html)
+ access_url = '%s?token=%s' % (CONF.spice.html5proxy_base_url,
+ token)
+ else:
+ raise exception.ConsoleTypeInvalid(console_type=console_type)
+
+ # Retrieve connect info from driver, and then decorate with our
+ # access info token
+ connect_info = self.driver.get_spice_console(instance)
+ connect_info['token'] = token
+ connect_info['access_url'] = access_url
+
+ return connect_info
+
def _attach_volume_boot(self, context, instance, volume, mountpoint):
"""Attach a volume to an instance at boot time. So actual attach
is done by instance creation"""
diff --git a/nova/compute/rpcapi.py b/nova/compute/rpcapi.py
index 3e7ed1cfd143..525d1adc741b 100644
--- a/nova/compute/rpcapi.py
+++ b/nova/compute/rpcapi.py
@@ -158,6 +158,7 @@ class ComputeAPI(nova.openstack.common.rpc.proxy.RpcProxy):
2.22 - Add recreate, on_shared_storage and host arguments to
rebuild_instance()
2.23 - Remove network_info from reboot_instance
+ 2.24 - Added get_spice_console method
'''
#
@@ -295,6 +296,13 @@ class ComputeAPI(nova.openstack.common.rpc.proxy.RpcProxy):
instance=instance_p, console_type=console_type),
topic=_compute_topic(self.topic, ctxt, None, instance))
+ def get_spice_console(self, ctxt, instance, console_type):
+ instance_p = jsonutils.to_primitive(instance)
+ return self.call(ctxt, self.make_msg('get_spice_console',
+ instance=instance_p, console_type=console_type),
+ topic=_compute_topic(self.topic, ctxt, None, instance),
+ version='2.24')
+
def host_maintenance_mode(self, ctxt, host_param, mode, host):
'''Set host maintenance mode
diff --git a/nova/tests/api/openstack/compute/contrib/test_consoles.py b/nova/tests/api/openstack/compute/contrib/test_consoles.py
index d251c6b75735..cf044dfcd9ab 100644
--- a/nova/tests/api/openstack/compute/contrib/test_consoles.py
+++ b/nova/tests/api/openstack/compute/contrib/test_consoles.py
@@ -26,19 +26,36 @@ def fake_get_vnc_console(self, _context, _instance, _console_type):
return {'url': 'http://fake'}
+def fake_get_spice_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)
+def fake_get_spice_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"])
+def fake_get_spice_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"])
+def fake_get_spice_console_not_found(self, _context, instance, _console_type):
+ raise exception.InstanceNotFound(instance_id=instance["uuid"])
+
+
def fake_get(self, context, instance_uuid):
return {'uuid': instance_uuid}
@@ -53,6 +70,8 @@ class ConsolesExtensionTest(test.TestCase):
super(ConsolesExtensionTest, self).setUp()
self.stubs.Set(compute_api.API, 'get_vnc_console',
fake_get_vnc_console)
+ self.stubs.Set(compute_api.API, 'get_spice_console',
+ fake_get_spice_console)
self.stubs.Set(compute_api.API, 'get', fake_get)
self.flags(
osapi_compute_extension=[
@@ -132,3 +151,76 @@ class ConsolesExtensionTest(test.TestCase):
res = req.get_response(self.app)
self.assertEqual(res.status_int, 400)
+
+ def test_get_spice_console(self):
+ body = {'os-getSPICEConsole': {'type': 'spice-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'spice-html5'}})
+
+ def test_get_spice_console_not_ready(self):
+ self.stubs.Set(compute_api.API, 'get_spice_console',
+ fake_get_spice_console_not_ready)
+ body = {'os-getSPICEConsole': {'type': 'spice-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_spice_console_no_type(self):
+ self.stubs.Set(compute_api.API, 'get_spice_console',
+ fake_get_spice_console_invalid_type)
+ body = {'os-getSPICEConsole': {}}
+ 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_spice_console_no_instance(self):
+ self.stubs.Set(compute_api.API, 'get', fake_get_not_found)
+ body = {'os-getSPICEConsole': {'type': 'spice-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_spice_console_no_instance_on_console_get(self):
+ self.stubs.Set(compute_api.API, 'get_spice_console',
+ fake_get_spice_console_not_found)
+ body = {'os-getSPICEConsole': {'type': 'spice-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_spice_console_invalid_type(self):
+ body = {'os-getSPICEConsole': {'type': 'invalid'}}
+ self.stubs.Set(compute_api.API, 'get_spice_console',
+ fake_get_spice_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/compute/test_compute.py b/nova/tests/compute/test_compute.py
index 0d9f67231893..3740d598e727 100644
--- a/nova/tests/compute/test_compute.py
+++ b/nova/tests/compute/test_compute.py
@@ -1335,6 +1335,9 @@ class ComputeTestCase(BaseTestCase):
def test_novnc_vnc_console(self):
# Make sure we can a vnc console for an instance.
+ self.flags(vnc_enabled=True)
+ self.flags(enabled=False, group='spice')
+
instance = jsonutils.to_primitive(self._create_fake_instance())
self.compute.run_instance(self.context, instance=instance)
@@ -1347,6 +1350,9 @@ class ComputeTestCase(BaseTestCase):
def test_xvpvnc_vnc_console(self):
# Make sure we can a vnc console for an instance.
+ self.flags(vnc_enabled=True)
+ self.flags(enabled=False, group='spice')
+
instance = jsonutils.to_primitive(self._create_fake_instance())
self.compute.run_instance(self.context, instance=instance)
@@ -1357,6 +1363,9 @@ class ComputeTestCase(BaseTestCase):
def test_invalid_vnc_console_type(self):
# Raise useful error if console type is an unrecognised string.
+ self.flags(vnc_enabled=True)
+ self.flags(enabled=False, group='spice')
+
instance = jsonutils.to_primitive(self._create_fake_instance())
self.compute.run_instance(self.context, instance=instance)
@@ -1367,6 +1376,9 @@ class ComputeTestCase(BaseTestCase):
def test_missing_vnc_console_type(self):
# Raise useful error is console type is None.
+ self.flags(vnc_enabled=True)
+ self.flags(enabled=False, group='spice')
+
instance = jsonutils.to_primitive(self._create_fake_instance())
self.compute.run_instance(self.context, instance=instance)
@@ -1375,6 +1387,47 @@ class ComputeTestCase(BaseTestCase):
self.context, None, instance=instance)
self.compute.terminate_instance(self.context, instance=instance)
+ def test_spicehtml5_spice_console(self):
+ # Make sure we can a spice console for an instance.
+ self.flags(vnc_enabled=False)
+ self.flags(enabled=True, group='spice')
+
+ instance = jsonutils.to_primitive(self._create_fake_instance())
+ self.compute.run_instance(self.context, instance=instance)
+
+ # Try with the full instance
+ console = self.compute.get_spice_console(self.context, 'spice-html5',
+ instance=instance)
+ self.assert_(console)
+
+ self.compute.terminate_instance(self.context, instance=instance)
+
+ def test_invalid_spice_console_type(self):
+ # Raise useful error if console type is an unrecognised string
+ self.flags(vnc_enabled=False)
+ self.flags(enabled=True, group='spice')
+
+ instance = jsonutils.to_primitive(self._create_fake_instance())
+ self.compute.run_instance(self.context, instance=instance)
+
+ self.assertRaises(exception.ConsoleTypeInvalid,
+ self.compute.get_spice_console,
+ self.context, 'invalid', instance=instance)
+ self.compute.terminate_instance(self.context, instance=instance)
+
+ def test_missing_spice_console_type(self):
+ # Raise useful error is console type is None
+ self.flags(vnc_enabled=False)
+ self.flags(enabled=True, group='spice')
+
+ instance = jsonutils.to_primitive(self._create_fake_instance())
+ self.compute.run_instance(self.context, instance=instance)
+
+ self.assertRaises(exception.ConsoleTypeInvalid,
+ self.compute.get_spice_console,
+ self.context, None, instance=instance)
+ self.compute.terminate_instance(self.context, instance=instance)
+
def test_diagnostics(self):
# Make sure we can get diagnostics for an instance.
expected_diagnostic = {'cpu0_time': 17300000000,
@@ -5512,6 +5565,50 @@ class ComputeAPITestCase(BaseTestCase):
db.instance_destroy(self.context, instance['uuid'])
+ def test_spice_console(self):
+ # Make sure we can a spice console for an instance.
+
+ fake_instance = {'uuid': 'fake_uuid',
+ 'host': 'fake_compute_host'}
+ fake_console_type = "spice-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'}
+ fake_connect_info2 = copy.deepcopy(fake_connect_info)
+ fake_connect_info2['access_url'] = 'fake_console_url'
+
+ self.mox.StubOutWithMock(rpc, 'call')
+
+ rpc_msg1 = {'method': 'get_spice_console',
+ 'args': {'instance': fake_instance,
+ 'console_type': fake_console_type},
+ 'version': '2.24'}
+ rpc_msg2 = {'method': 'authorize_console',
+ 'args': fake_connect_info,
+ 'version': '1.0'}
+
+ rpc.call(self.context, 'compute.%s' % fake_instance['host'],
+ rpc_msg1, None).AndReturn(fake_connect_info2)
+ rpc.call(self.context, CONF.consoleauth_topic,
+ rpc_msg2, None).AndReturn(None)
+
+ self.mox.ReplayAll()
+
+ console = self.compute_api.get_spice_console(self.context,
+ fake_instance, fake_console_type)
+ self.assertEqual(console, {'url': 'fake_console_url'})
+
+ def test_get_spice_console_no_host(self):
+ instance = self._create_fake_instance(params={'host': ''})
+
+ self.assertRaises(exception.InstanceNotReady,
+ self.compute_api.get_spice_console,
+ self.context, instance, 'spice')
+
+ db.instance_destroy(self.context, instance['uuid'])
+
def test_get_backdoor_port(self):
# Test api call to get backdoor_port.
fake_backdoor_port = 59697
diff --git a/nova/tests/compute/test_rpcapi.py b/nova/tests/compute/test_rpcapi.py
index 00b90ea65a5d..b81e049bfc1a 100644
--- a/nova/tests/compute/test_rpcapi.py
+++ b/nova/tests/compute/test_rpcapi.py
@@ -165,6 +165,11 @@ class ComputeRpcAPITestCase(test.TestCase):
self._test_compute_api('get_vnc_console', 'call',
instance=self.fake_instance, console_type='type')
+ def test_get_spice_console(self):
+ self._test_compute_api('get_spice_console', 'call',
+ instance=self.fake_instance, console_type='type',
+ version='2.24')
+
def test_host_maintenance_mode(self):
self._test_compute_api('host_maintenance_mode', 'call',
host_param='param', mode='mode', host='host')
diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py
index 15890cdcd50d..acefa856cd2f 100644
--- a/nova/tests/fake_policy.py
+++ b/nova/tests/fake_policy.py
@@ -42,6 +42,7 @@ policy_data = """
"compute:unlock": "",
"compute:get_vnc_console": "",
+ "compute:get_spice_console": "",
"compute:get_console_output": "",
"compute:associate_floating_ip": "",
diff --git a/nova/tests/fakelibvirt.py b/nova/tests/fakelibvirt.py
index 8d9561c7eca6..a573b7d1c93c 100644
--- a/nova/tests/fakelibvirt.py
+++ b/nova/tests/fakelibvirt.py
@@ -414,6 +414,7 @@ class Domain(object):
+