1 From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2 From: Dominik Csapak <d.csapak@proxmox.com>
3 Date: Tue, 13 Dec 2016 16:11:35 +0100
4 Subject: [PATCH] add PVE specific JS code
6 Add a ES6 module named `PVEUI` which defines the Proxmox VE
7 related helper methods, like for example, API2Request.
9 Hook the `PVEUI` module into the upstream `ui.js`, so that handlers
10 for `autoresizing`, `commandstoggle`, etc., get setup.
12 Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
13 Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
15 app/pve.js | 427 +++++++++++++++++++++++++++++++++++++++++++++++++++++
16 app/ui.js | 66 +++++++--
18 3 files changed, 489 insertions(+), 14 deletions(-)
19 create mode 100644 app/pve.js
21 diff --git a/app/pve.js b/app/pve.js
23 index 0000000..e3c7758
28 + * PVE Utility functions for noVNC
29 + * Copyright (C) 2017 Proxmox GmbH
32 +import * as WebUtil from "./webutil.js";
34 +export default function PVEUI(UI){
35 + this.consoletype = WebUtil.getQueryVar('console');
36 + this.vmid = WebUtil.getQueryVar('vmid');
37 + this.vmname = WebUtil.getQueryVar('vmname');
38 + this.nodename = WebUtil.getQueryVar('node');
39 + this.resize = WebUtil.getQueryVar('resize');
40 + this.lastFBWidth = undefined;
41 + this.lastFBHeight = undefined;
42 + this.sizeUpdateTimer = undefined;
45 + var baseUrl = '/nodes/' + this.nodename;
47 + var params = { websocket: 1 };
50 + switch (this.consoletype) {
52 + baseUrl += '/qemu/' + this.vmid;
53 + url = baseUrl + '/vncproxy';
54 + title = "VM " + this.vmid;
56 + title += " ('" + this.vmname + "')";
60 + baseUrl += '/lxc/' + this.vmid;
61 + url = baseUrl + '/vncproxy';
62 + title = "CT " + this.vmid;
64 + title += " ('" + this.vmname + "')";
68 + url = baseUrl + '/vncshell';
69 + title = "node '" + this.nodename + "'";
72 + url = baseUrl + '/vncshell';
74 + title = 'System upgrade on node ' + this.nodename;
77 + throw 'implement me';
81 + if (this.resize == 'scale' &&
82 + (this.consoletype === 'lxc' || this.consoletype === 'shell')) {
83 + var size = this.getFBSize();
84 + params.width = size.width;
85 + params.height = size.height;
88 + this.baseUrl = baseUrl;
90 + this.params = params;
91 + document.title = title;
95 + urlEncode: function(object) {
96 + var i,value, params = [];
99 + if (object.hasOwnProperty(i)) {
101 + if (value === undefined) value = '';
102 + params.push(encodeURIComponent(i) + '=' + encodeURIComponent(String(value)));
106 + return params.join('&');
109 + API2Request: function(reqOpts) {
112 + reqOpts.method = reqOpts.method || 'GET';
114 + var xhr = new XMLHttpRequest();
116 + xhr.onload = function() {
117 + var scope = reqOpts.scope || this;
121 + if (xhr.readyState === 4) {
122 + var ctype = xhr.getResponseHeader('Content-Type');
123 + if (xhr.status === 200) {
124 + if (ctype.match(/application\/json;/)) {
125 + result = JSON.parse(xhr.responseText);
127 + errmsg = 'got unexpected content type ' + ctype;
130 + errmsg = 'Error ' + xhr.status + ': ' + xhr.statusText;
133 + errmsg = 'Connection error - server offline?';
136 + if (errmsg !== undefined) {
137 + if (reqOpts.failure) {
138 + reqOpts.failure.call(scope, errmsg);
141 + if (reqOpts.success) {
142 + reqOpts.success.call(scope, result);
145 + if (reqOpts.callback) {
146 + reqOpts.callback.call(scope, errmsg === undefined);
150 + var data = me.urlEncode(reqOpts.params || {});
152 + if (reqOpts.method === 'GET') {
153 + xhr.open(reqOpts.method, "/api2/json" + reqOpts.url + '?' + data);
155 + xhr.open(reqOpts.method, "/api2/json" + reqOpts.url);
157 + xhr.setRequestHeader('Cache-Control', 'no-cache');
158 + if (reqOpts.method === 'POST' || reqOpts.method === 'PUT') {
159 + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
160 + xhr.setRequestHeader('CSRFPreventionToken', PVE.CSRFPreventionToken);
162 + } else if (reqOpts.method === 'GET') {
165 + throw "unknown method";
169 + pve_detect_migrated_vm: function() {
171 + if (me.consoletype === 'kvm') {
172 + // try to detect migrated VM
174 + url: '/cluster/resources',
176 + success: function(result) {
177 + var list = result.data;
178 + list.every(function(item) {
179 + if (item.type === 'qemu' && item.vmid == me.vmid) {
180 + var url = "?" + me.urlEncode({
181 + console: me.consoletype,
188 + location.href = url;
189 + return false; // break
195 + } else if(me.consoletype === 'lxc') {
196 + // lxc restart migration can take a while,
197 + // so we need to find out if we are really migrating
199 + var check = setInterval(function() {
200 + if (migrating === undefined ||
201 + migrating === true) {
202 + // check (again) if migrating
203 + me.UI.showStatus('Waiting for connection...', 'warning', 5000);
205 + url: me.baseUrl + '/config',
207 + success: function(result) {
208 + var lock = result.data.lock;
209 + if (lock == 'migrate') {
211 + me.UI.showStatus('Migration detected, waiting...', 'warning', 5000);
216 + failure: function() {
221 + // not migrating any more
222 + me.UI.showStatus('Connection resumed', 'warning');
223 + clearInterval(check);
225 + url: '/cluster/resources',
227 + success: function(result) {
228 + var list = result.data;
229 + list.every(function(item) {
230 + if (item.type === 'lxc' && item.vmid == me.vmid) {
231 + var url = "?" + me.urlEncode({
232 + console: me.consoletype,
239 + location.href = url;
240 + return false; // break
252 + pve_vm_command: function(cmd, params, reload) {
255 + var confirmMsg = "";
265 + confirmMsg = "Do you really want to " + cmd + " VM/CT {0}?";
271 + throw "implement me " + cmd;
274 + confirmMsg = confirmMsg.replace('{0}', me.vmid);
276 + if (confirmMsg !== "" && confirm(confirmMsg) !== true) {
280 + me.UI.closePVECommandPanel();
282 + if (me.consoletype === 'kvm') {
283 + baseUrl = '/nodes/' + me.nodename + '/qemu/' + me.vmid;
284 + } else if (me.consoletype === 'lxc') {
285 + baseUrl = '/nodes/' + me.nodename + '/lxc/' + me.vmid;
287 + throw "unknown VM type";
292 + url: baseUrl + "/status/" + cmd,
294 + failure: function(msg) {
295 + if (cmd === 'start' && msg.match(/already running/) !== null) {
296 + // we wanted to start, but it was already running, so
298 + me.UI.showStatus("VM command '" + cmd +"' successful", 'normal');
299 + setTimeout(function() {
303 + me.UI.showStatus(msg, 'warning');
306 + success: function() {
307 + me.UI.showStatus("VM command '" + cmd +"' successful", 'normal');
309 + setTimeout(function() {
317 + addPVEHandlers: function() {
319 + document.getElementById('pve_commands_button')
320 + .addEventListener('click', me.UI.togglePVECommandPanel);
322 + // show/hide the buttons
323 + document.getElementById('noVNC_disconnect_button')
324 + .classList.add('noVNC_hidden');
325 + if (me.consoletype === 'kvm') {
326 + document.getElementById('noVNC_clipboard_button')
327 + .classList.add('noVNC_hidden');
330 + if (me.consoletype === 'shell' || me.consoletype === 'upgrade') {
331 + document.getElementById('pve_commands_button')
332 + .classList.add('noVNC_hidden');
335 + // add command logic
336 + var commandArray = [
337 + { cmd: 'start', kvm: 1, lxc: 1},
338 + { cmd: 'stop', kvm: 1, lxc: 1},
339 + { cmd: 'shutdown', kvm: 1, lxc: 1},
340 + { cmd: 'suspend', kvm: 1},
341 + { cmd: 'resume', kvm: 1},
342 + { cmd: 'reset', kvm: 1},
343 + { cmd: 'reload', kvm: 1, lxc: 1, shell: 1},
346 + commandArray.forEach(function(item) {
347 + var el = document.getElementById('pve_command_'+item.cmd);
352 + if (item[me.consoletype] === 1) {
353 + el.onclick = function() {
354 + me.pve_vm_command(item.cmd);
357 + el.classList.add('noVNC_hidden');
362 + getFBSize: function() {
366 + if (window.innerHeight) {
367 + oh = window.innerHeight;
368 + ow = window.innerWidth;
369 + } else if (document.documentElement &&
370 + document.documentElement.clientHeight) {
371 + oh = document.documentElement.clientHeight;
372 + ow = document.documentElement.clientWidth;
373 + } else if (document.body) {
374 + oh = document.body.clientHeight;
375 + ow = document.body.clientWidth;
377 + throw "can't get window size";
380 + return { width: ow, height: oh };
383 + pveStart: function(callback) {
389 + success: function(result) {
390 + var wsparams = me.urlEncode({
391 + port: result.data.port,
392 + vncticket: result.data.ticket
395 + document.getElementById('noVNC_password_input').value = result.data.ticket;
396 + me.UI.forceSetting('path', 'api2/json' + me.baseUrl + '/vncwebsocket' + "?" + wsparams);
400 + failure: function(msg) {
401 + me.UI.showStatus(msg, 'error');
406 + updateFBSize: function(rfb, width, height) {
409 + // Note: window size must be even number for firefox
410 + me.lastFBWidth = Math.floor((width + 1)/2)*2;
411 + me.lastFBHeight = Math.floor((height + 1)/2)*2;
413 + if (me.sizeUpdateTimer !== undefined) {
414 + clearInterval(me.sizeUpdateTimer);
417 + var update_size = function() {
418 + var clip = me.UI.getSetting('view_clip');
419 + var resize = me.UI.getSetting('resize');
420 + var autoresize = me.UI.getSetting('autoresize');
421 + if (clip || resize === 'scale' || !autoresize) {
425 + // we do not want to resize if we are in fullscreen
426 + if (document.fullscreenElement || // alternative standard method
427 + document.mozFullScreenElement || // currently working methods
428 + document.webkitFullscreenElement ||
429 + document.msFullscreenElement) {
433 + var oldsize = me.getFBSize();
434 + var offsetw = me.lastFBWidth - oldsize.width;
435 + var offseth = me.lastFBHeight - oldsize.height;
436 + if (offsetw !== 0 || offseth !== 0) {
437 + //console.log("try resize by " + offsetw + " " + offseth);
439 + window.resizeBy(offsetw, offseth);
441 + console.log('resizing did not work', e);
447 + me.sizeUpdateTimer = setInterval(update_size, 1000);
454 diff --git a/app/ui.js b/app/ui.js
455 index cb6a9fd..6b4442f 100644
458 @@ -16,6 +16,7 @@ import keysyms from "../core/input/keysymdef.js";
459 import Keyboard from "../core/input/keyboard.js";
460 import RFB from "../core/rfb.js";
461 import * as WebUtil from "./webutil.js";
462 +import PVEUI from "./pve.js";
464 const PAGE_TITLE = "noVNC";
466 @@ -56,6 +57,8 @@ const UI = {
467 // Render default UI and initialize settings menu
470 + UI.PVE = new PVEUI(UI);
475 @@ -100,6 +103,9 @@ const UI = {
476 UI.addConnectionControlHandlers();
477 UI.addClipboardHandlers();
478 UI.addSettingsHandlers();
480 + // add pve specific event handlers
481 + UI.PVE.addPVEHandlers();
482 document.getElementById("noVNC_status")
483 .addEventListener('click', UI.hideStatus);
485 @@ -108,19 +114,15 @@ const UI = {
489 + UI.updateViewClip();
491 UI.updateVisualState('init');
493 document.documentElement.classList.remove("noVNC_loading");
495 - let autoconnect = WebUtil.getConfigVar('autoconnect', false);
496 - if (autoconnect === 'true' || autoconnect == '1') {
497 - autoconnect = true;
498 + UI.PVE.pveStart(function() {
501 - autoconnect = false;
502 - // Show the connect panel on first load unless autoconnecting
503 - UI.openConnectPanel();
507 return Promise.resolve(UI.rfb);
509 @@ -164,11 +166,12 @@ const UI = {
510 /* Populate the controls if defaults are provided in the URL */
511 UI.initSetting('host', window.location.hostname);
512 UI.initSetting('port', port);
513 - UI.initSetting('encrypt', (window.location.protocol === "https:"));
514 + UI.initSetting('encrypt', true);
515 UI.initSetting('view_clip', false);
516 UI.initSetting('resize', 'off');
517 UI.initSetting('quality', 6);
518 UI.initSetting('compression', 2);
519 + UI.initSetting('autoresize', true);
520 UI.initSetting('shared', true);
521 UI.initSetting('view_only', false);
522 UI.initSetting('show_dot', false);
523 @@ -347,6 +350,7 @@ const UI = {
524 UI.addSettingChangeHandler('resize');
525 UI.addSettingChangeHandler('resize', UI.applyResizeMode);
526 UI.addSettingChangeHandler('resize', UI.updateViewClip);
527 + UI.addSettingChangeHandler('autoresize');
528 UI.addSettingChangeHandler('quality');
529 UI.addSettingChangeHandler('quality', UI.updateQuality);
530 UI.addSettingChangeHandler('compression');
531 @@ -401,6 +405,9 @@ const UI = {
532 document.documentElement.classList.add("noVNC_connecting");
535 + UI.connected = true;
536 + UI.inhibit_reconnect = false;
537 + UI.pveAllowMigratedTest = true;
538 document.documentElement.classList.add("noVNC_connected");
540 case 'disconnecting':
541 @@ -408,6 +415,11 @@ const UI = {
542 document.documentElement.classList.add("noVNC_disconnecting");
545 + UI.showStatus(_("Disconnected"));
546 + if (UI.pveAllowMigratedTest === true) {
547 + UI.pveAllowMigratedTest = false;
548 + UI.PVE.pve_detect_migrated_vm();
552 transitionElem.textContent = _("Reconnecting...");
553 @@ -821,6 +833,7 @@ const UI = {
554 UI.closePowerPanel();
555 UI.closeClipboardPanel();
557 + UI.closePVECommandPanel();
561 @@ -998,6 +1011,12 @@ const UI = {
562 UI.reconnectPassword = password;
565 + var password = document.getElementById('noVNC_password_input').value;
568 + password = WebUtil.getConfigVar('password');
571 if (password === null) {
572 password = undefined;
574 @@ -1622,9 +1641,36 @@ const UI = {
582 + togglePVECommandPanel: function() {
583 + if (document.getElementById('pve_commands').classList.contains("noVNC_open")) {
584 + UI.closePVECommandPanel();
586 + UI.openPVECommandPanel();
590 + openPVECommandPanel: function() {
592 + UI.closeAllPanels();
593 + UI.openControlbar();
595 + document.getElementById('pve_commands').classList.add("noVNC_open");
596 + document.getElementById('pve_commands_button').classList.add("noVNC_selected");
599 + closePVECommandPanel: function() {
600 + document.getElementById('pve_commands').classList.remove("noVNC_open");
601 + document.getElementById('pve_commands_button').classList.remove("noVNC_selected");
611 UI.rfb.viewOnly = UI.getSetting('view_only');
612 diff --git a/vnc.html b/vnc.html
613 index 8d4b497..7ce9ba7 100644
617 <li class="noVNC_heading">
618 <img alt="" src="app/images/settings.svg"> Settings
621 + <li style="display:none;">
622 <label><input id="noVNC_setting_shared" type="checkbox"> Shared Mode</label>
625 @@ -173,16 +173,18 @@
627 <label><input id="noVNC_setting_view_clip" type="checkbox"> Clip to Window</label>
630 + <label><input id="noVNC_setting_autoresize" type="checkbox" /> Autoresize Window</label>
633 <label for="noVNC_setting_resize">Scaling Mode:</label>
634 <select id="noVNC_setting_resize" name="vncResize">
635 - <option value="off">None</option>
636 + <option value="off">Off</option>
637 <option value="scale">Local Scaling</option>
638 - <option value="remote">Remote Resizing</option>
643 + <li style="display:none;">
644 <div class="noVNC_expander">Advanced</div>