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:
parent
9f89dc4f37
commit
59b7e6011b
@ -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,
|
||||
|
@ -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'),
|
||||
)
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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])
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 %}
|
@ -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 %}
|
@ -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',
|
||||
|
@ -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'),
|
||||
)
|
||||
|
@ -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'
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user