Commit 9dcd479e by Őry Máté

Merge branch 'issue-87' into 'master'

Console facelift

* get token on tab opening
* push ctrl on connection to switch off screensaver
* refactor js to use jqeury instead of mixed dom+jquery
parents ce74d490 aa3f2c7a
[
{
"pk": 1,
"model": "firewall.vlan",
"fields": {
"comment": "",
"ipv6_template": "2001:7:2:4031:%(b)d:%(c)d:%(d)d:0",
"domain": 1,
"dhcp_pool": "",
"managed": true,
"name": "pub",
"vid": 3066,
"created_at": "2014-02-19T17:00:17.358Z",
"modified_at": "2014-02-19T17:00:17.358Z",
"owner": null,
"snat_ip": null,
"snat_to": [],
"network6": null,
"network4": "10.7.0.93/16",
"reverse_domain": "%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa",
"network_type": "public",
"description": ""
}
},
{
"pk": 1,
"model": "firewall.host",
"fields": {
"comment": "",
"vlan": 1,
"reverse": "",
"created_at": "2014-02-19T17:03:45.365Z",
"hostname": "devenv",
"modified_at": "2014-02-24T15:55:01.412Z",
"location": "",
"pub_ipv4": null,
"mac": "11:22:33:44:55:66",
"shared_ip": false,
"ipv4": "10.7.0.96",
"groups": [],
"ipv6": null,
"owner": 1,
"description": ""
}
},
{
"pk": 1,
"model": "firewall.domain",
"fields": {
"description": "",
"created_at": "2014-02-19T17:00:08.819Z",
"modified_at": "2014-02-19T17:00:08.819Z",
"ttl": 600,
"owner": 1,
"name": "test.ik.bme.hu"
}
},
{
"pk": 1,
"model": "vm.node",
"fields": {
"name": "devenv",
"created": "2014-02-19T17:03:45.322Z",
"overcommit": 1.0,
"enabled": true,
"modified": "2014-02-19T21:11:34.671Z",
"priority": 1,
"traits": [],
"host": 1
}
}
]
$(function() {
"use strict";
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
"input.js", "display.js", "jsunzip.js", "rfb.js"]);
var rfb;
function updateState(rfb, state, oldstate, msg) {
$('#_console .btn-toolbar button').attr('disabled', !(state === "normal"));
rfb.sendKey(0xffe3); // press and release ctrl to kill screensaver
if (typeof(msg) !== 'undefined') {
$('#noVNC_status').html(msg);
}
}
$('a[data-toggle$="pill"][href!="#console"]').click(function() {
if (rfb) {
rfb.disconnect();
rfb = 0;
}
$("#vm-info-pane").fadeIn();
$("#vm-detail-pane").removeClass("col-md-12");
});
$('#sendCtrlAltDelButton').click(function() {
rfb.sendCtrlAltDel(); return false;});
$('#sendPasswordButton').click(function() {
var pw = $("#vm-details-pw-input").val();
for (var i=0; i < pw.length; i++) {
rfb.sendKey(pw.charCodeAt(i));
} return false;});
$("body").on("click", 'a[href$="console"]', function() {
var host, port, password, path;
$("#vm-info-pane").hide();
$("#vm-detail-pane").addClass("col-md-12");
WebUtil.init_logging('warn');
host = window.location.hostname;
if (window.location.port == 8080) {
port = 9999;
} else {
port = window.location.port == "" ? "443" : window.location.port;
}
password = '';
$('#_console .btn-toolbar button').attr('disabled', true);
$('#noVNC_status').html('Retreiving authorization token.');
$.get(VNC_URL, function(data) {
if (data.indexOf('vnc') != 0) {
$('#noVNC_status').html('No authorization token received.');
}
else {
rfb = new RFB({'target': $D('noVNC_canvas'),
'encrypt': (window.location.protocol === "https:"),
'true_color': true,
'local_cursor': true,
'shared': true,
'view_only': false,
'updateState': updateState});
rfb.connect(host, port, password, data);
}
}).fail(function(){
$('#noVNC_status').html("Can't connect to console.");
});
});
if (window.location.hash == "#console")
window.onscriptsload = function(){$('a[href$="console"]').click();};
});
...@@ -80,7 +80,7 @@ ...@@ -80,7 +80,7 @@
<dt>Password:</dt> <dt>Password:</dt>
<dd> <dd>
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control input-sm input-tags" value="{{ instance.pw }}"/> <input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags" value="{{ instance.pw }}"/>
<span class="input-group-addon input-tags" id="vm-details-pw-show"> <span class="input-group-addon input-tags" id="vm-details-pw-show">
<i class="icon-eye-open" id="vm-details-pw-eye" title="Show password"></i> <i class="icon-eye-open" id="vm-details-pw-eye" title="Show password"></i>
</span> </span>
...@@ -113,8 +113,8 @@ ...@@ -113,8 +113,8 @@
<i class="icon-tasks icon-2x"></i><br> <i class="icon-tasks icon-2x"></i><br>
{% trans "Resources" %}</a> {% trans "Resources" %}</a>
</li> </li>
<li {% if instance.state != "RUNNING" %}class="disabled"{% endif %}> <li {% if not instance.is_console_available %}class="disabled"{% endif %}>
<a href="#{% if instance.state == "RUNNING" %}console" data-toggle="pill{% endif %}" data-target="#_console" class="text-center"> <a href="#console" data-toggle="pill" data-target="#_console" class="text-center">
<i class="icon-desktop icon-2x"></i><br> <i class="icon-desktop icon-2x"></i><br>
{% trans "Console" %}</a></li> {% trans "Console" %}</a></li>
<li> <li>
...@@ -152,4 +152,5 @@ ...@@ -152,4 +152,5 @@
{% block extra_js %} {% block extra_js %}
<script src="{{ STATIC_URL }}dashboard/vm-details.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-details.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-common.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-common.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-console.js"></script>
{% endblock %} {% endblock %}
<div class="btn-toolbar"> <div class="btn-toolbar">
<button id="sendCtrlAltDelButton" class="btn btn-danger small" href="#">Send CtrlAltDel</button> <button id="sendCtrlAltDelButton" class="btn btn-danger small">Send CtrlAltDel</button>
<button id="sendPasswordButton" class="btn btn-default small" href="#">Type password</button> <button id="sendPasswordButton" class="btn btn-default small">Type password</button>
</div> </div>
<div class="alert alert-info" id="noVNC_status"> <div class="alert alert-info" id="noVNC_status">
</div> </div>
...@@ -10,68 +10,6 @@ ...@@ -10,68 +10,6 @@
<script src="{{ STATIC_URL }}dashboard/novnc/util.js"></script> <script src="{{ STATIC_URL }}dashboard/novnc/util.js"></script>
<script> <script>
"use strict";
var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/'; var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/';
var VNC_URL = "{{ vnc_url }}";
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", </script>
"input.js", "display.js", "jsunzip.js", "rfb.js"]);
var rfb;
function updateState(rfb, state, oldstate, msg) {
var s, sb, cad
s = $('#noVNC_status')[0];
cad = $('#sendCtrlAltDelButton')[0];
if (state === "normal") { cad.disabled = false; }
else { cad.disabled = true; }
if (typeof(msg) !== 'undefined') {
s.innerHTML = msg;
}
}
$('a[data-toggle$="pill"][href!="#console"]').click(function() {
if (rfb) {
rfb.disconnect();
rfb = 0;
}
$("#vm-info-pane").fadeIn();
$("#vm-detail-pane").removeClass("col-md-12");
});
$('#sendCtrlAltDelButton').click(function() {
rfb.sendCtrlAltDel(); return false;});
$('#sendPasswordButton').click(function() {
var pw = '{{instance.pw}}';
for (var i=0; i < pw.length; i++) {
rfb.sendKey(pw.charCodeAt(i));
} return false;});
$("body").on("click", 'a[href$="console"]', function() {
var host, port, password, path;
$("#vm-info-pane").hide();
$("#vm-detail-pane").addClass("col-md-12");
WebUtil.init_logging('warn');
host = window.location.hostname;
if (window.location.port == 8080) {
port = 9999;
} else {
port = window.location.port == "" ? "443" : window.location.port;
}
password = '';
path = 'vnc/?d={{ vnc_url }}';
rfb = new RFB({'target': $D('noVNC_canvas'),
'encrypt': (window.location.protocol === "https:"),
'true_color': true,
'local_cursor': true,
'shared': true,
'view_only': false,
'updateState': updateState});
rfb.connect(host, port, password, path);
});
</script>
...@@ -2,7 +2,7 @@ from django.test import TestCase ...@@ -2,7 +2,7 @@ from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from vm.models import Instance, InstanceTemplate, Lease from vm.models import Instance, InstanceTemplate, Lease, Node
from storage.models import Disk from storage.models import Disk
from firewall.models import Vlan from firewall.models import Vlan
...@@ -234,3 +234,37 @@ class VmDetailTest(TestCase): ...@@ -234,3 +234,37 @@ class VmDetailTest(TestCase):
response = c.get("/dashboard/notifications/") response = c.get("/dashboard/notifications/")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
assert self.u1.notification_set.get().status == 'read' assert self.u1.notification_set.get().status == 'read'
class VmDetailVncTest(TestCase):
fixtures = ['test-vm-fixture.json', 'node.json']
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.set_password('password')
self.u1.save()
def login(self, client, username, password='password'):
response = client.post('/accounts/login/', {'username': username,
'password': password})
self.assertNotEqual(response.status_code, 403)
def test_permitted_vm_console(self):
c = Client()
self.login(c, 'user1')
inst = Instance.objects.get(pk=1)
inst.node = Node.objects.all()[0]
inst.save()
inst.set_level(self.u1, 'operator')
response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 200)
def test_not_permitted_vm_console(self):
c = Client()
self.login(c, 'user1')
inst = Instance.objects.get(pk=1)
inst.node = Node.objects.all()[0]
inst.save()
inst.set_level(self.u1, 'user')
response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 403)
...@@ -9,7 +9,7 @@ from .views import ( ...@@ -9,7 +9,7 @@ from .views import (
FavouriteView, NodeStatus, GroupList, TemplateDelete, LeaseDelete, FavouriteView, NodeStatus, GroupList, TemplateDelete, LeaseDelete,
VmGraphView, TemplateAclUpdateView, GroupDetailView, GroupDelete, VmGraphView, TemplateAclUpdateView, GroupDetailView, GroupDelete,
GroupAclUpdateView, GroupUserDelete, NotificationView, NodeGraphView, GroupAclUpdateView, GroupUserDelete, NotificationView, NodeGraphView,
VmMigrateView, VmMigrateView, VmDetailVncTokenView,
) )
urlpatterns = patterns( urlpatterns = patterns(
...@@ -37,6 +37,8 @@ urlpatterns = patterns( ...@@ -37,6 +37,8 @@ urlpatterns = patterns(
name='dashboard.views.remove-port'), name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(), url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
name='dashboard.views.detail'), name='dashboard.views.detail'),
url(r'^vm/(?P<pk>\d+)/vnctoken/$', VmDetailVncTokenView.as_view(),
name='dashboard.views.detail-vnc'),
url(r'^vm/(?P<pk>\d+)/acl/$', AclUpdateView.as_view(model=Instance), url(r'^vm/(?P<pk>\d+)/acl/$', AclUpdateView.as_view(model=Instance),
name='dashboard.views.vm-acl'), name='dashboard.views.vm-acl'),
url(r'^vm/(?P<pk>\d+)/tx/$', TransferOwnershipView.as_view(), url(r'^vm/(?P<pk>\d+)/tx/$', TransferOwnershipView.as_view(),
......
...@@ -148,6 +148,25 @@ class CheckedDetailView(LoginRequiredMixin, DetailView): ...@@ -148,6 +148,25 @@ class CheckedDetailView(LoginRequiredMixin, DetailView):
return context return context
class VmDetailVncTokenView(CheckedDetailView):
template_name = "dashboard/vm-detail.html"
model = Instance
def get(self, request, **kwargs):
self.object = self.get_object()
if not self.object.has_level(request.user, 'operator'):
raise PermissionDenied()
if self.object.node:
port = self.object.vnc_port
host = str(self.object.node.host.ipv4)
value = signing.dumps({'host': host,
'port': port},
key=getenv("PROXY_SECRET", 'asdasd')),
return HttpResponse('vnc/?d=%s' % value)
else:
raise Http404()
class VmDetailView(CheckedDetailView): class VmDetailView(CheckedDetailView):
template_name = "dashboard/vm-detail.html" template_name = "dashboard/vm-detail.html"
model = Instance model = Instance
...@@ -155,15 +174,10 @@ class VmDetailView(CheckedDetailView): ...@@ -155,15 +174,10 @@ class VmDetailView(CheckedDetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(VmDetailView, self).get_context_data(**kwargs) context = super(VmDetailView, self).get_context_data(**kwargs)
instance = context['instance'] instance = context['instance']
if instance.node:
port = instance.vnc_port
host = str(instance.node.host.ipv4)
value = signing.dumps({'host': host,
'port': port},
key=getenv("PROXY_SECRET", 'asdasd')),
context.update({ context.update({
'graphite_enabled': VmGraphView.get_graphite_url() is not None, 'graphite_enabled': VmGraphView.get_graphite_url() is not None,
'vnc_url': '%s' % value 'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
kwargs={'pk': self.object.pk})
}) })
# activity data # activity data
......
...@@ -270,6 +270,9 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -270,6 +270,9 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
act = None act = None
return 'NOSTATE' if act is None else act.resultant_state return 'NOSTATE' if act is None else act.resultant_state
def is_console_available(self):
return self.state in ('RUNNING', )
def manual_state_change(self, new_state, reason=None, user=None): def manual_state_change(self, new_state, reason=None, user=None):
# TODO cancel concurrent activity (if exists) # TODO cancel concurrent activity (if exists)
act = InstanceActivity.create(code_suffix='manual_state_change', act = InstanceActivity.create(code_suffix='manual_state_change',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment