Merge "Serial Console"

This commit is contained in:
Jenkins 2015-02-19 06:36:49 +00:00 committed by Gerrit Code Review
commit 738cdf26ef
14 changed files with 266 additions and 15 deletions

View File

@ -326,11 +326,13 @@ If you do not have multiple regions you should use the ``OPENSTACK_HOST`` and
Default: ``"AUTO"``
This setting specifies the type of in-browser VNC console used to access the
This setting specifies the type of in-browser console used to access the
VMs.
Valid values are ``"AUTO"``(default), ``"VNC"``, ``"SPICE"``, ``"RDP"``
and ``None`` (this latest value is available in version 2014.2(Juno) to allow
deactivating the in-browser console).
Valid values are ``"AUTO"``(default), ``"VNC"``, ``"SPICE"``, ``"RDP"``,
``"SERIAL"``, and ``None``.
``None`` deactivates the in-browser console and is available in version
2014.2(Juno).
``"SERIAL"`` is available since 2005.1(Kilo).
``INSTANCE_LOG_LENGTH``

View File

@ -0,0 +1,95 @@
/*
Copyright 2014, Rackspace, US, Inc.
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.
*/
/*global Terminal,Blob,FileReader,gettext,interpolate */
(function() {
'use strict';
angular.module('serialConsoleApp', [])
.constant('protocols', ['binary', 'base64'])
.constant('states', [gettext('Connecting'), gettext('Open'), gettext('Closing'), gettext('Closed')])
/**
* @ngdoc directive
* @ngname serialConsole
*
* @description
* The serial-console element creates a terminal based on the widely-used term.js.
* The "connection" attribute is input to a WebSocket object, which connects
* to a server. In Horizon, this directive is used to connect to nova-serialproxy,
* opening a serial console to any instance. Each key the user types is transmitted
* to the instance, and each character the instance reponds with is displayed.
*/
.directive('serialConsole', function(protocols, states) {
return {
scope: true,
template: '<div id="terminalNode"></div><br>{{statusMessage()}}',
restrict: 'E',
link: function postLink(scope, element, attrs) {
var connection = scope.$eval(attrs.connection);
var term = new Terminal();
var socket = new WebSocket(connection, protocols);
socket.onerror = function() {
scope.$apply(scope.status);
};
socket.onopen = function() {
scope.$apply(scope.status);
// initialize by "hitting enter"
socket.send(String.fromCharCode(13));
};
socket.onclose = function() {
scope.$apply(scope.status);
};
// turn the angular jQlite element into a raw DOM element so we can
// attach the Terminal to it
var termElement = angular.element(element)[0];
term.open(termElement.ownerDocument.getElementById('terminalNode'));
term.on('data', function(data) {
socket.send(data);
});
socket.onmessage = function(e) {
if (e.data instanceof Blob) {
var f = new FileReader();
f.onload = function() {
term.write(f.result);
};
f.readAsText(e.data);
} else {
term.write(e.data);
}
};
scope.status = function() {
return states[socket.readyState];
};
scope.statusMessage = function() {
return interpolate(gettext('Status: %s'), [scope.status()]);
};
scope.$on('$destroy', function() {
socket.close();
});
}
};
});
}());

View File

@ -75,6 +75,14 @@ class RDPConsole(base.APIDictWrapper):
_attrs = ['url', 'type']
class SerialConsole(base.APIDictWrapper):
"""Wrapper for the "console" dictionary.
Returned by the novaclient.servers.get_serial_console method.
"""
_attrs = ['url', 'type']
class Server(base.APIResourceWrapper):
"""Simple wrapper around novaclient.server.Server.
@ -455,6 +463,11 @@ def server_rdp_console(request, instance_id, console_type='rdp-html5'):
instance_id, console_type)['console'])
def server_serial_console(request, instance_id, console_type='serial'):
return SerialConsole(novaclient(request).servers.get_serial_console(
instance_id, console_type)['console'])
def flavor_create(request, name, memory, vcpu, disk, flavorid='auto',
ephemeral=0, swap=0, metadata=None, is_public=True):
flavor = novaclient(request).flavors.create(name, memory, vcpu, disk,

View File

@ -27,7 +27,8 @@ LOG = logging.getLogger(__name__)
CONSOLES = SortedDict([('VNC', api.nova.server_vnc_console),
('SPICE', api.nova.server_spice_console),
('RDP', api.nova.server_rdp_console)])
('RDP', api.nova.server_rdp_console),
('SERIAL', api.nova.server_serial_console)])
def get_console(request, console_type, instance):
@ -58,10 +59,14 @@ def get_console(request, console_type, instance):
LOG.debug('Console not available', exc_info=True)
continue
console_url = "%s&%s(%s)" % (
console.url,
urlencode({'title': getattr(instance, "name", "")}),
instance.id)
if con_type == 'SERIAL':
console_url = console.url
else:
console_url = "%s&%s(%s)" % (
console.url,
urlencode({'title': getattr(instance, "name", "")}),
instance.id)
return (con_type, console_url)
raise exceptions.NotAvailable(_('No available console found.'))

View File

@ -68,12 +68,17 @@ class ConsoleTab(tabs.Tab):
console_type = getattr(settings, 'CONSOLE_TYPE', 'AUTO')
console_url = None
try:
console_url = console.get_console(request, console_type,
instance)[1]
console_type, console_url = console.get_console(
request, console_type, instance)
# For serial console, the url is different from VNC, etc.
# because it does not include parms for title and token
if console_type == "SERIAL":
console_url = "/project/instances/%s/serial" % (instance.id)
except exceptions.NotAvailable:
exceptions.handle(request, ignore=True, force_log=True)
return {'console_url': console_url, 'instance_id': instance.id}
return {'console_url': console_url, 'instance_id': instance.id,
'console_type': console_type}
def allowed(self, request):
# The ConsoleTab is available if settings.CONSOLE_TYPE is not set at

View File

@ -3,7 +3,11 @@
<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><br />
<p class='alert alert-info'>
{% if console_type != 'SERIAL' %}
{% blocktrans %}If console is not responding to keyboard input: click the grey status bar below.{% endblocktrans %}
{% endif %}
<a href="{{ console_url }}" style="text-decoration: underline">{% trans "Click here to show only console" %}</a><br />
{% trans "To exit the fullscreen mode, click the browser's back button." %}</p>
<iframe id="console_embed" src="{{ console_url }}" style="width:100%;height:100%"></iframe>
<script type="text/javascript">

View File

@ -0,0 +1,24 @@
{% load i18n %}
<!DOCTYPE html>
<html>
<head>
<meta content='IE=edge' http-equiv='X-UA-Compatible' />
<meta content='text/html; charset=utf-8' http-equiv='Content-Type' />
<title>{{instance_name}} ({{instance_id}})</title>
<link rel="stylesheet" href="{{ STATIC_URL }}dashboard/scss/serial_console.css" type="text/css" media="screen">
<script src="{% url 'horizon:jsi18n' 'horizon' %}"></script>
<script src='{{ STATIC_URL }}horizon/lib/term.js'></script>
<script src="{{ STATIC_URL }}horizon/lib/jquery/jquery.js"></script>
<script src="{{ STATIC_URL }}horizon/lib/angular/angular.js"></script>
<script src="{{ STATIC_URL }}horizon/js/angular/directives/serialConsole.js"></script>
</head>
<body ng-app='serialConsoleApp'>
{% if error_message %}
{{ error_message }}
{% else %}
<serial-console connection='"{{console_url}}"'></serial-console>
{% endif %}
</body>
</html>

View File

@ -4252,7 +4252,8 @@ class ConsoleManagerTests(helpers.TestCase):
console.CONSOLES = SortedDict([
('VNC', api.nova.server_vnc_console),
('SPICE', api.nova.server_spice_console),
('RDP', api.nova.server_rdp_console)])
('RDP', api.nova.server_rdp_console),
('SERIAL', api.nova.server_serial_console)])
def _get_console_vnc(self, server):
console_mock = self.mox.CreateMock(api.nova.VNCConsole)
@ -4308,6 +4309,24 @@ class ConsoleManagerTests(helpers.TestCase):
data = console.get_console(self.request, 'RDP', server)[1]
self.assertEqual(data, url)
def _get_console_serial(self, server):
console_mock = self.mox.CreateMock(api.nova.SerialConsole)
console_mock.url = '/SERIAL'
self.mox.StubOutWithMock(api.nova, 'server_serial_console')
api.nova.server_serial_console(IgnoreArg(), server.id) \
.AndReturn(console_mock)
self.mox.ReplayAll()
self.setup_consoles()
def test_get_console_serial(self):
server = self.servers.first()
self._get_console_serial(server)
url = '/SERIAL'
data = console.get_console(self.request, 'SERIAL', server)[1]
self.assertEqual(data, url)
def test_get_console_auto_iterate_available(self):
server = self.servers.first()
@ -4333,6 +4352,35 @@ class ConsoleManagerTests(helpers.TestCase):
data = console.get_console(self.request, 'AUTO', server)[1]
self.assertEqual(data, url)
def test_get_console_auto_iterate_serial_available(self):
server = self.servers.first()
console_mock = self.mox.CreateMock(api.nova.SerialConsole)
console_mock.url = '/SERIAL'
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) \
.AndRaise(self.exceptions.nova)
self.mox.StubOutWithMock(api.nova, 'server_serial_console')
api.nova.server_serial_console(IgnoreArg(), server.id) \
.AndReturn(console_mock)
self.mox.ReplayAll()
self.setup_consoles()
url = '/SERIAL'
data = console.get_console(self.request, 'AUTO', server)[1]
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

@ -35,6 +35,8 @@ urlpatterns = patterns(
views.DetailView.as_view(), name='detail'),
url(INSTANCES % 'update', views.UpdateView.as_view(), name='update'),
url(INSTANCES % 'rebuild', views.RebuildView.as_view(), name='rebuild'),
url(INSTANCES % 'serial', views.SerialConsoleView.as_view(),
name='serial'),
url(INSTANCES % 'console', 'console', name='console'),
url(INSTANCES % 'vnc', 'vnc', name='vnc'),
url(INSTANCES % 'spice', 'spice', name='spice'),

View File

@ -25,6 +25,7 @@ from django import http
from django import shortcuts
from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext_lazy as _
from django.views import generic
from horizon import exceptions
from horizon import forms
@ -193,6 +194,35 @@ def rdp(request, instance_id):
exceptions.handle(request, msg, redirect=redirect)
class SerialConsoleView(generic.TemplateView):
template_name = 'project/instances/serial_console.html'
def get_context_data(self, **kwargs):
context = super(SerialConsoleView, self).get_context_data(**kwargs)
context['instance_id'] = self.kwargs['instance_id']
instance = None
try:
instance = api.nova.server_get(context['view'].request,
self.kwargs['instance_id'])
except Exception:
context["error_message"] = _(
"Cannot find instance %s.") % self.kwargs['instance_id']
# name is unknown, so leave it blank for the window title
# in full-screen mode, so only the instance id is shown.
context['instance_name'] = ''
return context
context['instance_name'] = instance.name
try:
console_url = project_console.get_console(context['view'].request,
"SERIAL", instance)[1]
context["console_url"] = console_url
except exceptions.NotAvailable:
context["error_message"] = _(
"Cannot get console for instance %s.") % self.kwargs[
'instance_id']
return context
class UpdateView(workflows.WorkflowView):
workflow_class = project_workflows.UpdateInstance
success_url = reverse_lazy("horizon:project:instances:index")

View File

@ -47,7 +47,7 @@ TEMPLATE_DEBUG = DEBUG
# OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = 'Default'
# Set Console type:
# valid options would be "AUTO"(default), "VNC", "SPICE", "RDP" or None
# valid options would be "AUTO"(default), "VNC", "SPICE", "RDP", "SERIAL" or None
# Set to None explicitly if you want to deactivate the console.
# CONSOLE_TYPE = "AUTO"

View File

@ -0,0 +1,19 @@
/* Stand-alone CSS file for Serial Console IFrame. */
#terminalNode {
display:inline-block;
}
.terminal {
float: left;
border: black solid 5px;
font-family: "DejaVu Sans Mono", "Liberation Mono", monospace;
font-size: 16px;
color: white;
background: black;
}
.terminal-cursor {
color: black;
background: white;
};

View File

@ -36,6 +36,7 @@ import xstatic.pkg.jsencrypt
import xstatic.pkg.qunit
import xstatic.pkg.rickshaw
import xstatic.pkg.spin
import xstatic.pkg.termjs
STATICFILES_DIRS = [
@ -73,6 +74,8 @@ STATICFILES_DIRS = [
xstatic.main.XStatic(xstatic.pkg.rickshaw).base_dir),
('horizon/lib',
xstatic.main.XStatic(xstatic.pkg.spin).base_dir),
('horizon/lib',
xstatic.main.XStatic(xstatic.pkg.termjs).base_dir),
]

View File

@ -58,3 +58,4 @@ XStatic-QUnit>=1.14.0.2 # MIT License
XStatic-Rickshaw>=1.5.0 # BSD License (prior)
XStatic-smart-table>=1.4.5.3 # MIT License
XStatic-Spin>=1.2.5.2 # MIT License
XStatic-term.js>=0.0.4 # MIT License