From 59b7e6011b0c392a29a06e736e42b0277ab3e719 Mon Sep 17 00:00:00 2001 From: "Daniel P. Berrange" Date: Wed, 2 Jan 2013 18:35:04 +0000 Subject: [PATCH] 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 --- openstack_dashboard/api/nova.py | 12 +++++++ .../dashboards/admin/instances/urls.py | 1 + .../dashboards/admin/instances/views.py | 2 +- .../dashboards/project/instances/tables.py | 6 ++-- .../dashboards/project/instances/tabs.py | 33 ++++++++++------- .../templates/instances/_detail_console.html | 21 +++++++++++ .../templates/instances/_detail_vnc.html | 21 ----------- .../dashboards/project/instances/tests.py | 36 +++++++++++++++++++ .../dashboards/project/instances/urls.py | 3 +- .../dashboards/project/instances/views.py | 12 +++++++ .../test/api_tests/nova_tests.py | 18 +++++++++- .../test/test_data/nova_data.py | 6 +++- 12 files changed, 130 insertions(+), 41 deletions(-) create mode 100644 openstack_dashboard/dashboards/project/instances/templates/instances/_detail_console.html delete mode 100644 openstack_dashboard/dashboards/project/instances/templates/instances/_detail_vnc.html diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 7393b23e2f..6c94b923f3 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -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, diff --git a/openstack_dashboard/dashboards/admin/instances/urls.py b/openstack_dashboard/dashboards/admin/instances/urls.py index 3e18d0c632..8999c9dca4 100644 --- a/openstack_dashboard/dashboards/admin/instances/urls.py +++ b/openstack_dashboard/dashboards/admin/instances/urls.py @@ -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'), ) diff --git a/openstack_dashboard/dashboards/admin/instances/views.py b/openstack_dashboard/dashboards/admin/instances/views.py index a11f96f5da..4248d39477 100644 --- a/openstack_dashboard/dashboards/admin/instances/views.py +++ b/openstack_dashboard/dashboards/admin/instances/views.py @@ -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__) diff --git a/openstack_dashboard/dashboards/project/instances/tables.py b/openstack_dashboard/dashboards/project/instances/tables.py index 6caffc8106..2870d0252a 100644 --- a/openstack_dashboard/dashboards/project/instances/tables.py +++ b/openstack_dashboard/dashboards/project/instances/tables.py @@ -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]) diff --git a/openstack_dashboard/dashboards/project/instances/tabs.py b/openstack_dashboard/dashboards/project/instances/tabs.py index fd15ff8547..029c877f06 100644 --- a/openstack_dashboard/dashboards/project/instances/tabs.py +++ b/openstack_dashboard/dashboards/project/instances/tabs.py @@ -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 diff --git a/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_console.html b/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_console.html new file mode 100644 index 0000000000..bc0a07be80 --- /dev/null +++ b/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_console.html @@ -0,0 +1,21 @@ +{% load i18n %} + +

{% trans "Instance Console" %}

+{% if console_url %} +

{% blocktrans %}If console is not responding to keyboard input: click the grey status bar below.{% endblocktrans %} {% trans "Click here to show only console" %}

+ + +{% else %} +

{% blocktrans %}console is currently unavailable. Please try again later.{% endblocktrans %} +{% trans "Reload" %}

+{% endif %} diff --git a/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_vnc.html b/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_vnc.html deleted file mode 100644 index 12a714c62c..0000000000 --- a/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_vnc.html +++ /dev/null @@ -1,21 +0,0 @@ -{% load i18n %} - -

{% trans "Instance VNC Console" %}

-{% if vnc_url %} -

{% blocktrans %}If VNC console is not responding to keyboard input: click the grey status bar below.{% endblocktrans %} {% trans "Show only VNC" %}

- - -{% else %} -

{% blocktrans %}VNC console is currently unavailable. Please try again later.{% endblocktrans %} -{% trans "Reload" %}

-{% endif %} diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index f5eea1005d..ed575f89e1 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -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', diff --git a/openstack_dashboard/dashboards/project/instances/urls.py b/openstack_dashboard/dashboards/project/instances/urls.py index 8ea1609a93..461c04c448 100644 --- a/openstack_dashboard/dashboards/project/instances/urls.py +++ b/openstack_dashboard/dashboards/project/instances/urls.py @@ -33,5 +33,6 @@ urlpatterns = patterns(VIEW_MOD, url(r'^(?P[^/]+)/$', 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'), ) diff --git a/openstack_dashboard/dashboards/project/instances/views.py b/openstack_dashboard/dashboards/project/instances/views.py index 4a0799e200..448dc42a4b 100644 --- a/openstack_dashboard/dashboards/project/instances/views.py +++ b/openstack_dashboard/dashboards/project/instances/views.py @@ -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' diff --git a/openstack_dashboard/test/api_tests/nova_tests.py b/openstack_dashboard/test/api_tests/nova_tests.py index 1e979c46e3..9e4c963459 100644 --- a/openstack_dashboard/test/api_tests/nova_tests.py +++ b/openstack_dashboard/test/api_tests/nova_tests.py @@ -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() diff --git a/openstack_dashboard/test/test_data/nova_data.py b/openstack_dashboard/test/test_data/nova_data.py index 42288bd3ad..89b2ef2a09 100644 --- a/openstack_dashboard/test/test_data/nova_data.py +++ b/openstack_dashboard/test/test_data/nova_data.py @@ -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,