Add support for SPICE consoles

While in theory both VNC and SPICE can be enabled at the same
time, this is not expected to be common. Thus, rather than
adding a 'SPICE console' tab, this renames the existing
'VNC console' tab to simply be 'Console'. This tab is setup
to prefer exposing a VNC console, but if that is not enabled,
then expose the SPICE console. The reason for this order is
that the noVNC widget has had much more testing than the
current spice-html5 widget. Thus if both VNC & SPICE are
enabled, VNC is likely a more reliable choice at this point
in time.

Blueprint: libvirt-spice
Change-Id: If3d3769fe8e29c5930ac8b42d841c92182c4be72
Signed-off-by: Daniel P. Berrange <berrange@redhat.com>
This commit is contained in:
Daniel P. Berrange 2013-01-02 18:35:04 +00:00
parent 9f89dc4f37
commit 59b7e6011b
12 changed files with 130 additions and 41 deletions

View File

@ -53,6 +53,13 @@ class VNCConsole(APIDictWrapper):
_attrs = ['url', 'type']
class SPICEConsole(APIDictWrapper):
"""Wrapper for the "console" dictionary returned by the
novaclient.servers.get_spice_console method.
"""
_attrs = ['url', 'type']
class Server(APIResourceWrapper):
"""Simple wrapper around novaclient.server.Server
@ -195,6 +202,11 @@ def server_vnc_console(request, instance_id, console_type='novnc'):
console_type)['console'])
def server_spice_console(request, instance_id, console_type='spice-html5'):
return SPICEConsole(novaclient(request).servers.get_spice_console(
instance_id, console_type)['console'])
def flavor_create(request, name, memory, vcpu, disk, ephemeral=0, swap=0,
metadata=None):
flavor = novaclient(request).flavors.create(name, memory, vcpu, disk,

View File

@ -31,4 +31,5 @@ urlpatterns = patterns('openstack_dashboard.dashboards.admin.instances.views',
url(INSTANCES % 'detail', DetailView.as_view(), name='detail'),
url(INSTANCES % 'console', 'console', name='console'),
url(INSTANCES % 'vnc', 'vnc', name='vnc'),
url(INSTANCES % 'spice', 'spice', name='spice'),
)

View File

@ -31,7 +31,7 @@ from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.instances.tables import \
AdminInstancesTable
from openstack_dashboard.dashboards.project.instances.views import \
console, DetailView, vnc
console, DetailView, vnc, spice
LOG = logging.getLogger(__name__)

View File

@ -33,7 +33,7 @@ from horizon.utils.filters import replace_underscores
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.access_and_security \
.floating_ips.workflows import IPAssociationWorkflow
from .tabs import InstanceDetailTabs, LogTab, VNCTab
from .tabs import InstanceDetailTabs, LogTab, ConsoleTab
LOG = logging.getLogger(__name__)
@ -217,7 +217,7 @@ class CreateSnapshot(tables.LinkAction):
class ConsoleLink(tables.LinkAction):
name = "console"
verbose_name = _("VNC Console")
verbose_name = _("Console")
url = "horizon:project:instances:detail"
classes = ("btn-console",)
@ -226,7 +226,7 @@ class ConsoleLink(tables.LinkAction):
def get_link_url(self, datum):
base_url = super(ConsoleLink, self).get_link_url(datum)
tab_query_string = VNCTab(InstanceDetailTabs).get_query_string()
tab_query_string = ConsoleTab(InstanceDetailTabs).get_query_string()
return "?".join([base_url, tab_query_string])

View File

@ -51,28 +51,35 @@ class LogTab(tabs.Tab):
"console_log": data}
class VNCTab(tabs.Tab):
name = _("VNC")
slug = "vnc"
template_name = "project/instances/_detail_vnc.html"
class ConsoleTab(tabs.Tab):
name = _("Console")
slug = "console"
template_name = "project/instances/_detail_console.html"
preload = False
def get_context_data(self, request):
instance = self.tab_group.kwargs['instance']
# Currently prefer VNC over SPICE, since noVNC has had much more
# testing than spice-html5
try:
console = api.nova.server_vnc_console(request, instance.id)
vnc_url = "%s&title=%s(%s)" % (console.url,
getattr(instance, "name", ""),
instance.id)
console_url = "%s&title=%s(%s)" % (
console.url,
getattr(instance, "name", ""),
instance.id)
except:
vnc_url = None
exceptions.handle(request,
_('Unable to get VNC console for '
'instance "%s".') % instance.id)
return {'vnc_url': vnc_url, 'instance_id': instance.id}
try:
console = api.nova.server_spice_console(request, instance.id)
console_url = "%s&title=%s(%s)" % (
console.url,
getattr(instance, "name", ""),
instance.id)
except:
console_url = None
return {'console_url': console_url, 'instance_id': instance.id}
class InstanceDetailTabs(tabs.TabGroup):
slug = "instance_details"
tabs = (OverviewTab, LogTab, VNCTab)
tabs = (OverviewTab, LogTab, ConsoleTab)
sticky = True

View File

@ -0,0 +1,21 @@
{% load i18n %}
<h3>{% trans "Instance Console" %}</h3>
{% if console_url %}
<p class='alert alert-info'>{% blocktrans %}If console is not responding to keyboard input: click the grey status bar below.{% endblocktrans %} <a href="{{ console_url }}" style="text-decoration: underline">{% trans "Click here to show only console" %}</a></p>
<iframe id="console_embed" src="{{ console_url }}" style="width:100%;height:100%"></iframe>
<script type="text/javascript">
var fix_height = function() {
$('iframe#console_embed').css({ height: $(document).height() + 'px' });
};
// there are two code paths to this particular block; handle them both
if (typeof($) != 'undefined') {
$(document).ready(fix_height);
} else {
addHorizonLoadEvent(fix_height);
}
</script>
{% else %}
<p class='alert alert-error'>{% blocktrans %}console is currently unavailable. Please try again later.{% endblocktrans %}
<a class='btn btn-mini' href="{% url horizon:project:instances:detail instance_id %}">{% trans "Reload" %}</a></p>
{% endif %}

View File

@ -1,21 +0,0 @@
{% load i18n %}
<h3>{% trans "Instance VNC Console" %}</h3>
{% if vnc_url %}
<p class='alert alert-info'>{% blocktrans %}If VNC console is not responding to keyboard input: click the grey status bar below.{% endblocktrans %} <a href="{{ vnc_url }}" style="text-decoration: underline">{% trans "Show only VNC" %}</a></p>
<iframe id="vnc_console" src="{{ vnc_url }}" style="width:100%;height:100%"></iframe>
<script type="text/javascript">
var fix_height = function() {
$('iframe#vnc_console').css({ height: $(document).height() + 'px' });
};
// there are two code paths to this particular block; handle them both
if (typeof($) != 'undefined') {
$(document).ready(fix_height);
} else {
addHorizonLoadEvent(fix_height);
}
</script>
{% else %}
<p class='alert alert-error'>{% blocktrans %}VNC console is currently unavailable. Please try again later.{% endblocktrans %}
<a class='btn btn-mini' href="{% url horizon:project:instances:detail instance_id %}">{% trans "Reload" %}</a></p>
{% endif %}

View File

@ -531,6 +531,42 @@ class InstanceTests(test.TestCase):
self.assertRedirectsNoFollow(res, INDEX_URL)
def test_instance_spice(self):
server = self.servers.first()
CONSOLE_OUTPUT = '/spiceserver'
console_mock = self.mox.CreateMock(api.nova.SPICEConsole)
console_mock.url = CONSOLE_OUTPUT
self.mox.StubOutWithMock(api.nova, 'server_spice_console')
self.mox.StubOutWithMock(api.nova, 'server_get')
api.nova.server_get(IsA(http.HttpRequest), server.id) \
.AndReturn(server)
api.nova.server_spice_console(IgnoreArg(), server.id) \
.AndReturn(console_mock)
self.mox.ReplayAll()
url = reverse('horizon:project:instances:spice',
args=[server.id])
res = self.client.get(url)
redirect = CONSOLE_OUTPUT + '&title=%s(1)' % server.name
self.assertRedirectsNoFollow(res, redirect)
@test.create_stubs({api.nova: ('server_spice_console',)})
def test_instance_spice_exception(self):
server = self.servers.first()
api.nova.server_spice_console(IsA(http.HttpRequest), server.id) \
.AndRaise(self.exceptions.nova)
self.mox.ReplayAll()
url = reverse('horizon:project:instances:spice',
args=[server.id])
res = self.client.get(url)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api.nova: ('server_get',
'snapshot_create',
'server_list',

View File

@ -33,5 +33,6 @@ urlpatterns = patterns(VIEW_MOD,
url(r'^(?P<instance_id>[^/]+)/$', DetailView.as_view(), name='detail'),
url(INSTANCES % 'update', UpdateView.as_view(), name='update'),
url(INSTANCES % 'console', 'console', name='console'),
url(INSTANCES % 'vnc', 'vnc', name='vnc')
url(INSTANCES % 'vnc', 'vnc', name='vnc'),
url(INSTANCES % 'spice', 'spice', name='spice'),
)

View File

@ -123,6 +123,18 @@ def vnc(request, instance_id):
exceptions.handle(request, msg, redirect=redirect)
def spice(request, instance_id):
try:
console = api.nova.server_spice_console(request, instance_id)
instance = api.nova.server_get(request, instance_id)
return shortcuts.redirect(console.url +
("&title=%s(%s)" % (instance.name, instance_id)))
except:
redirect = reverse("horizon:project:instances:index")
msg = _('Unable to get SPICE console for instance "%s".') % instance_id
exceptions.handle(request, msg, redirect=redirect)
class UpdateView(forms.ModalFormView):
form_class = UpdateInstance
template_name = 'project/instances/update.html'

View File

@ -61,7 +61,7 @@ class ComputeApiTests(test.APITestCase):
def test_server_vnc_console(self):
server = self.servers.first()
console = self.servers.console_data
console = self.servers.vnc_console_data
console_type = console["console"]["type"]
novaclient = self.stub_novaclient()
@ -75,6 +75,22 @@ class ComputeApiTests(test.APITestCase):
console_type)
self.assertIsInstance(ret_val, api.nova.VNCConsole)
def test_server_spice_console(self):
server = self.servers.first()
console = self.servers.spice_console_data
console_type = console["console"]["type"]
novaclient = self.stub_novaclient()
novaclient.servers = self.mox.CreateMockAnything()
novaclient.servers.get_spice_console(server.id,
console_type).AndReturn(console)
self.mox.ReplayAll()
ret_val = api.nova.server_spice_console(self.request,
server.id,
console_type)
self.assertIsInstance(ret_val, api.nova.SPICEConsole)
def test_server_list(self):
servers = self.servers.list()

View File

@ -346,7 +346,11 @@ def data(TEST):
# VNC Console Data
console = {u'console': {u'url': u'http://example.com:6080/vnc_auto.html',
u'type': u'novnc'}}
TEST.servers.console_data = console
TEST.servers.vnc_console_data = console
# SPICE Console Data
console = {u'console': {u'url': u'http://example.com:6080/spice_auto.html',
u'type': u'spice'}}
TEST.servers.spice_console_data = console
# Floating IPs
fip_1 = floating_ips.FloatingIP(floating_ips.FloatingIPManager(None),
{'id': 1,