diff --git a/doc/source/topics/settings.rst b/doc/source/topics/settings.rst
index 377a728ad6..063350d07c 100755
--- a/doc/source/topics/settings.rst
+++ b/doc/source/topics/settings.rst
@@ -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``
diff --git a/horizon/static/horizon/js/angular/directives/serialConsole.js b/horizon/static/horizon/js/angular/directives/serialConsole.js
new file mode 100644
index 0000000000..e50282f23d
--- /dev/null
+++ b/horizon/static/horizon/js/angular/directives/serialConsole.js
@@ -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: '
{{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();
+ });
+
+ }
+ };
+ });
+}());
\ No newline at end of file
diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py
index 0e1221dc36..15bf8cb74c 100644
--- a/openstack_dashboard/api/nova.py
+++ b/openstack_dashboard/api/nova.py
@@ -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,
diff --git a/openstack_dashboard/dashboards/project/instances/console.py b/openstack_dashboard/dashboards/project/instances/console.py
index 5d68f50760..d865212b4c 100644
--- a/openstack_dashboard/dashboards/project/instances/console.py
+++ b/openstack_dashboard/dashboards/project/instances/console.py
@@ -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.'))
diff --git a/openstack_dashboard/dashboards/project/instances/tabs.py b/openstack_dashboard/dashboards/project/instances/tabs.py
index c81f6599b9..e58b4c5b47 100644
--- a/openstack_dashboard/dashboards/project/instances/tabs.py
+++ b/openstack_dashboard/dashboards/project/instances/tabs.py
@@ -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
diff --git a/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_console.html b/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_console.html
index e6cf5be198..28cddcc6e9 100644
--- a/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_console.html
+++ b/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_console.html
@@ -3,7 +3,11 @@
{% 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" %}
+
+{% if console_type != 'SERIAL' %}
+{% blocktrans %}If console is not responding to keyboard input: click the grey status bar below.{% endblocktrans %}
+{% endif %}
+{% trans "Click here to show only console" %}
{% trans "To exit the fullscreen mode, click the browser's back button." %}
+
+
+
+
+
+
+
+ {% if error_message %}
+ {{ error_message }}
+ {% else %}
+
+ {% endif %}
+
+
+