Refactor code around Console support

Add a console module to handle the different kinds on remote consoles
supported (vnc, rdp, spice ) and refactor the code
to use console.get_console.

In the future, a matrix based on HyperVisor could be added
in case that only one type of console is supported.
Ex: HyperV <=> RDP

Change-Id: If448b7c9d953765b9b56ee14b39975d951ffd92c
Implements: blueprint refactor-console-support
Closes-Bug: #1287881
This commit is contained in:
Leandro I. Costantino 2014-03-05 10:56:25 -03:00 committed by Leandro Ignacio Costantino
parent cacd93ea49
commit ded54e6f5d
5 changed files with 217 additions and 93 deletions

View File

@ -169,6 +169,11 @@ class AlreadyExists(HorizonException):
return _(self.msg) % self.attrs
class NotAvailable(HorizonException):
"""Exception to be raised when something is not available."""
pass
class WorkflowError(HorizonException):
"""Exception to be raised when something goes wrong in a workflow."""
pass
@ -191,7 +196,7 @@ class HandledException(HorizonException):
UNAUTHORIZED = tuple(HORIZON_CONFIG['exceptions']['unauthorized'])
NOT_FOUND = tuple(HORIZON_CONFIG['exceptions']['not_found'])
RECOVERABLE = (AlreadyExists, Conflict,)
RECOVERABLE = (AlreadyExists, Conflict, NotAvailable)
RECOVERABLE += tuple(HORIZON_CONFIG['exceptions']['recoverable'])

View File

@ -0,0 +1,62 @@
#
# 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.
import logging
from django.utils.datastructures import SortedDict
from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from novaclient import exceptions as nova_exception
from openstack_dashboard import api
LOG = logging.getLogger(__name__)
CONSOLES = SortedDict([('VNC', api.nova.server_vnc_console),
('SPICE', api.nova.server_spice_console),
('RDP', api.nova.server_rdp_console)])
def get_console(request, console_type, instance):
"""Get a console url based on console type."""
if console_type == 'AUTO':
check_consoles = CONSOLES
else:
try:
check_consoles = {'console_type': CONSOLES[console_type]}
except KeyError:
msg = _('Console type "%s" not supported.') % console_type
LOG.error(msg)
raise exceptions.NotAvailable(msg)
for api_call in check_consoles.values():
try:
console = api_call(request, instance.id)
#if not supported don't log it to avoid lot of errors
#in case of AUTO
except nova_exception.HTTPNotImplemented:
continue
except Exception as e:
LOG.exception(e)
continue
console_url = "%s&%s(%s)" % (
console.url,
urlencode({'title': getattr(instance, "name", "")}),
instance.id)
return console_url
raise exceptions.NotAvailable(_('No available console found.'))

View File

@ -19,6 +19,7 @@ from horizon import exceptions
from horizon import tabs
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.instances import console
class OverviewTab(tabs.Tab):
@ -58,63 +59,12 @@ class ConsoleTab(tabs.Tab):
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
console_type = getattr(settings, 'CONSOLE_TYPE', 'AUTO')
if console_type == 'AUTO':
try:
console = api.nova.server_vnc_console(request, instance.id)
console_url = "%s&title=%s(%s)" % (
console.url,
getattr(instance, "name", ""),
instance.id)
except Exception:
try:
console = api.nova.server_spice_console(request,
instance.id)
console_url = "%s&title=%s(%s)" % (
console.url,
getattr(instance, "name", ""),
instance.id)
except Exception:
try:
console = api.nova.server_rdp_console(request,
instance.id)
console_url = "%s&title=%s(%s)" % (
console.url,
getattr(instance, "name", ""),
instance.id)
except Exception:
console_url = None
elif console_type == 'VNC':
try:
console = api.nova.server_vnc_console(request, instance.id)
console_url = "%s&title=%s(%s)" % (
console.url,
getattr(instance, "name", ""),
instance.id)
except Exception:
console_url = None
elif console_type == 'SPICE':
try:
console = api.nova.server_spice_console(request, instance.id)
console_url = "%s&title=%s(%s)" % (
console.url,
getattr(instance, "name", ""),
instance.id)
except Exception:
console_url = None
elif console_type == 'RDP':
try:
console = api.nova.server_rdp_console(request, instance.id)
console_url = "%s&title=%s(%s)" % (
console.url,
getattr(instance, "name", ""),
instance.id)
except Exception:
console_url = None
else:
console_url = None
console_url = None
try:
console_url = console.get_console(request, console_type, instance)
except exceptions.NotAvailable:
pass
return {'console_url': console_url, 'instance_id': instance.id}

View File

@ -29,16 +29,18 @@ from django.utils.http import urlencode
from mox import IgnoreArg # noqa
from mox import IsA # noqa
from horizon import exceptions
from horizon.workflows import views
from openstack_dashboard import api
from openstack_dashboard.api import cinder
from openstack_dashboard.test import helpers as test
from openstack_dashboard.usage import quotas
from openstack_dashboard.dashboards.project.instances import console
from openstack_dashboard.dashboards.project.instances import tables
from openstack_dashboard.dashboards.project.instances import tabs
from openstack_dashboard.dashboards.project.instances import workflows
from openstack_dashboard.test import helpers as test
from openstack_dashboard.usage import quotas
INDEX_URL = reverse('horizon:project:instances:index')
SEC_GROUP_ROLE_PREFIX = \
@ -809,30 +811,35 @@ class InstanceTests(test.TestCase):
def test_instance_vnc(self):
server = self.servers.first()
CONSOLE_OUTPUT = '/vncserver'
CONSOLE_TITLE = '&title=%s(%s)' % (server.name, server.id)
CONSOLE_URL = CONSOLE_OUTPUT + CONSOLE_TITLE
console_mock = self.mox.CreateMock(api.nova.VNCConsole)
console_mock.url = CONSOLE_OUTPUT
self.mox.StubOutWithMock(api.nova, 'server_vnc_console')
self.mox.StubOutWithMock(api.nova, 'server_get')
self.mox.StubOutWithMock(console, 'get_console')
api.nova.server_get(IsA(http.HttpRequest), server.id) \
.AndReturn(server)
api.nova.server_vnc_console(IgnoreArg(), server.id) \
.AndReturn(console_mock)
console.get_console(IgnoreArg(), 'VNC', server) \
.AndReturn(CONSOLE_URL)
self.mox.ReplayAll()
url = reverse('horizon:project:instances:vnc',
args=[server.id])
res = self.client.get(url)
redirect = CONSOLE_OUTPUT + '&title=%s(1)' % server.name
redirect = CONSOLE_URL
self.assertRedirectsNoFollow(res, redirect)
@test.create_stubs({api.nova: ('server_vnc_console',)})
def test_instance_vnc_exception(self):
def test_instance_vnc_error(self):
server = self.servers.first()
api.nova.server_vnc_console(IsA(http.HttpRequest), server.id) \
.AndRaise(self.exceptions.nova)
self.mox.StubOutWithMock(api.nova, 'server_get')
self.mox.StubOutWithMock(console, 'get_console')
api.nova.server_get(IsA(http.HttpRequest), server.id) \
.AndReturn(server)
console.get_console(IgnoreArg(), 'VNC', server) \
.AndRaise(exceptions.NotAvailable('console'))
self.mox.ReplayAll()
@ -845,30 +852,35 @@ class InstanceTests(test.TestCase):
def test_instance_spice(self):
server = self.servers.first()
CONSOLE_OUTPUT = '/spiceserver'
CONSOLE_TITLE = '&title=%s(%s)' % (server.name, server.id)
CONSOLE_URL = CONSOLE_OUTPUT + CONSOLE_TITLE
console_mock = self.mox.CreateMock(api.nova.SPICEConsole)
console_mock.url = CONSOLE_OUTPUT
self.mox.StubOutWithMock(api.nova, 'server_spice_console')
self.mox.StubOutWithMock(console, 'get_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)
console.get_console(IgnoreArg(), 'SPICE', server) \
.AndReturn(CONSOLE_URL)
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
redirect = CONSOLE_URL
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.StubOutWithMock(console, 'get_console')
self.mox.StubOutWithMock(api.nova, 'server_get')
api.nova.server_get(IsA(http.HttpRequest), server.id) \
.AndReturn(server)
console.get_console(IgnoreArg(), 'SPICE', server) \
.AndRaise(exceptions.NotAvailable('console'))
self.mox.ReplayAll()
@ -881,30 +893,36 @@ class InstanceTests(test.TestCase):
def test_instance_rdp(self):
server = self.servers.first()
CONSOLE_OUTPUT = '/rdpserver'
CONSOLE_TITLE = '&title=%s(%s)' % (server.name, server.id)
CONSOLE_URL = CONSOLE_OUTPUT + CONSOLE_TITLE
console_mock = self.mox.CreateMock(api.nova.RDPConsole)
console_mock.url = CONSOLE_OUTPUT
self.mox.StubOutWithMock(api.nova, 'server_rdp_console')
self.mox.StubOutWithMock(console, 'get_console')
self.mox.StubOutWithMock(api.nova, 'server_get')
api.nova.server_get(IsA(http.HttpRequest), server.id) \
.AndReturn(server)
api.nova.server_rdp_console(IgnoreArg(), server.id) \
.AndReturn(console_mock)
console.get_console(IgnoreArg(), 'RDP', server) \
.AndReturn(CONSOLE_URL)
self.mox.ReplayAll()
url = reverse('horizon:project:instances:rdp',
args=[server.id])
res = self.client.get(url)
redirect = CONSOLE_OUTPUT + '&title=%s(1)' % server.name
redirect = CONSOLE_URL
self.assertRedirectsNoFollow(res, redirect)
@test.create_stubs({api.nova: ('server_rdp_console',)})
def test_instance_rdp_exception(self):
server = self.servers.first()
api.nova.server_rdp_console(IsA(http.HttpRequest), server.id) \
.AndRaise(self.exceptions.nova)
self.mox.StubOutWithMock(console, 'get_console')
self.mox.StubOutWithMock(api.nova, 'server_get')
api.nova.server_get(IsA(http.HttpRequest), server.id) \
.AndReturn(server)
console.get_console(IgnoreArg(), 'RDP', server) \
.AndRaise(exceptions.NotAvailable('console'))
self.mox.ReplayAll()
@ -2913,3 +2931,92 @@ class InstanceAjaxTests(test.TestCase):
# a different availability zone.', u'']]
self.assertEqual(messages[0][0], 'error')
self.assertTrue(messages[0][1].startswith('Failed'))
class ConsoleManagerTests(test.TestCase):
def setup_consoles(self):
#need to refresh with mocks or will fail since mox do not detect
#the api_call() as mocked
console.CONSOLES = SortedDict([
('VNC', api.nova.server_vnc_console),
('SPICE', api.nova.server_spice_console),
('RDP', api.nova.server_rdp_console)])
def test_get_console_vnc(self):
server = self.servers.first()
console_mock = self.mox.CreateMock(api.nova.VNCConsole)
console_mock.url = '/VNC'
self.mox.StubOutWithMock(api.nova, 'server_vnc_console')
api.nova.server_vnc_console(IgnoreArg(), server.id) \
.AndReturn(console_mock)
self.mox.ReplayAll()
self.setup_consoles()
url = '/VNC&title=%s(%s)' % (server.name, server.id)
data = console.get_console(self.request, 'VNC', server)
self.assertEqual(data, url)
def test_get_console_spice(self):
server = self.servers.first()
console_mock = self.mox.CreateMock(api.nova.SPICEConsole)
console_mock.url = '/SPICE'
self.mox.StubOutWithMock(api.nova, 'server_spice_console')
api.nova.server_spice_console(IgnoreArg(), server.id) \
.AndReturn(console_mock)
self.mox.ReplayAll()
self.setup_consoles()
url = '/SPICE&title=%s(%s)' % (server.name, server.id)
data = console.get_console(self.request, 'SPICE',
server)
self.assertEqual(data, url)
def test_get_console_rdp(self):
server = self.servers.first()
console_mock = self.mox.CreateMock(api.nova.RDPConsole)
console_mock.url = '/RDP'
self.mox.StubOutWithMock(api.nova, 'server_rdp_console')
api.nova.server_rdp_console(IgnoreArg(), server.id) \
.AndReturn(console_mock)
self.mox.ReplayAll()
self.setup_consoles()
url = '/RDP&title=%s(%s)' % (server.name, server.id)
data = console.get_console(self.request, 'RDP', server)
self.assertEqual(data, url)
def test_get_console_auto_iterate_available(self):
server = self.servers.first()
console_mock = self.mox.CreateMock(api.nova.RDPConsole)
console_mock.url = '/RDP'
self.mox.StubOutWithMock(api.nova, 'server_vnc_console')
api.nova.server_vnc_console(IgnoreArg(), server.id) \
.AndRaise(self.exceptions.nova)
self.mox.StubOutWithMock(api.nova, 'server_spice_console')
api.nova.server_spice_console(IgnoreArg(), server.id) \
.AndRaise(self.exceptions.nova)
self.mox.StubOutWithMock(api.nova, 'server_rdp_console')
api.nova.server_rdp_console(IgnoreArg(), server.id) \
.AndReturn(console_mock)
self.mox.ReplayAll()
self.setup_consoles()
url = '/RDP&title=%s(%s)' % (server.name, server.id)
data = console.get_console(self.request, 'AUTO', server)
self.assertEqual(data, url)
def test_invalid_console_type_raise_value_error(self):
self.assertRaises(exceptions.NotAvailable,
console.get_console, None, 'FAKE', None)

View File

@ -34,6 +34,9 @@ from horizon.utils import memoized
from horizon import workflows
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.instances \
import console as project_console
from openstack_dashboard.dashboards.project.instances \
import forms as project_forms
from openstack_dashboard.dashboards.project.instances \
@ -149,10 +152,9 @@ def console(request, instance_id):
def vnc(request, instance_id):
try:
console = api.nova.server_vnc_console(request, instance_id)
instance = api.nova.server_get(request, instance_id)
return shortcuts.redirect(console.url +
("&title=%s(%s)" % (instance.name, instance_id)))
console_url = project_console.get_console(request, 'VNC', instance)
return shortcuts.redirect(console_url)
except Exception:
redirect = reverse("horizon:project:instances:index")
msg = _('Unable to get VNC console for instance "%s".') % instance_id
@ -161,10 +163,9 @@ def vnc(request, instance_id):
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)))
console_url = project_console.get_console(request, 'SPICE', instance)
return shortcuts.redirect(console_url)
except Exception:
redirect = reverse("horizon:project:instances:index")
msg = _('Unable to get SPICE console for instance "%s".') % instance_id
@ -173,10 +174,9 @@ def spice(request, instance_id):
def rdp(request, instance_id):
try:
console = api.nova.server_rdp_console(request, instance_id)
instance = api.nova.server_get(request, instance_id)
return shortcuts.redirect(console.url +
("&title=%s(%s)" % (instance.name, instance_id)))
console_url = project_console.get_console(request, 'RDP', instance)
return shortcuts.redirect(console_url)
except Exception:
redirect = reverse("horizon:project:instances:index")
msg = _('Unable to get RDP console for instance "%s".') % instance_id