From 94198be481f8062207e5d71a83606c3d5383a323 Mon Sep 17 00:00:00 2001 From: Frederick Borges Date: Thu, 10 Sep 2020 13:36:24 +0200 Subject: [PATCH] F #2410: Integrate vm autorefresh in sunstone-server (#201) * Websocket autorefresh * Integrate autorefresh in sunstone-server Signed-off-by: Frederick Borges --- install.sh | 4 +- share/install_gems/Gemfile | 1 + src/sunstone/etc/sunstone-server.conf | 9 +- src/sunstone/public/app/app.js | 3 + src/sunstone/public/app/sunstone-config.js | 3 + src/sunstone/public/app/sunstone.js | 16 ++- .../public/app/tabs/vms-tab/panels/info.js | 4 + .../app/tabs/vms-tab/panels/info/html.hbs | 4 +- src/sunstone/public/app/utils/websocket.js | 112 ++++++++++++++++++ src/sunstone/sunstone-server.rb | 62 +++++++++- src/sunstone/views/index.erb | 6 +- 11 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 src/sunstone/public/app/utils/websocket.js diff --git a/install.sh b/install.sh index f0294466286..8e41bfff728 100755 --- a/install.sh +++ b/install.sh @@ -460,7 +460,8 @@ VAR_DIRS="$VAR_LOCATION/remotes \ SUNSTONE_DIRS="$SUNSTONE_LOCATION/routes \ $SUNSTONE_LOCATION/models \ $SUNSTONE_LOCATION/models/OpenNebulaJSON \ - $SUNSTONE_LOCATION/views" + $SUNSTONE_LOCATION/views \ + $SUNSTONE_LOCATION/services" SUNSTONE_MINIFIED_DIRS="$SUNSTONE_LOCATION/public \ $SUNSTONE_LOCATION/public/dist \ @@ -735,6 +736,7 @@ INSTALL_SUNSTONE_FILES=( SUNSTONE_MODELS_JSON_FILES:$SUNSTONE_LOCATION/models/OpenNebulaJSON SUNSTONE_VIEWS_FILES:$SUNSTONE_LOCATION/views SUNSTONE_ROUTES_FILES:$SUNSTONE_LOCATION/routes + SUNSTONE_SERVICES_FILES:$SUNSTONE_LOCATION/services ) INSTALL_SUNSTONE_PUBLIC_MINIFIED_FILES=( diff --git a/share/install_gems/Gemfile b/share/install_gems/Gemfile index 2f8c3d8633b..4e3743e933c 100644 --- a/share/install_gems/Gemfile +++ b/share/install_gems/Gemfile @@ -124,6 +124,7 @@ group :sunstone do gem 'memcache-client' gem 'dalli' gem 'rotp' + gem 'sinatra-websocket' end group :oca do diff --git a/src/sunstone/etc/sunstone-server.conf b/src/sunstone/etc/sunstone-server.conf index 4b6b58752a9..d52f556bc4c 100644 --- a/src/sunstone/etc/sunstone-server.conf +++ b/src/sunstone/etc/sunstone-server.conf @@ -257,4 +257,11 @@ # This change the thresholds of dashboard resource usage :threshold_min: 0 :threshold_low: 33 -:threshold_high: 66 \ No newline at end of file +:threshold_high: 66 + +################################################################################ +# Autorefresh websocket configuration +################################################################################ + +:zeromq_server: tcp://localhost:2101 +:autorefresh_ip: 127.0.0.1 \ No newline at end of file diff --git a/src/sunstone/public/app/app.js b/src/sunstone/public/app/app.js index 5f2cf79c489..d18384ec155 100644 --- a/src/sunstone/public/app/app.js +++ b/src/sunstone/public/app/app.js @@ -39,6 +39,7 @@ define(function(require) { var Menu = require('utils/menu'); var Locale = require('utils/locale'); var UserAndZoneTemplate = require('hbs!sunstone/user_and_zone'); + var Websocket = require("utils/websocket"); var _commonDialogs = [ require('utils/dialogs/confirm'), @@ -73,6 +74,8 @@ define(function(require) { Sunstone.showTab(PROVISION_TAB_ID); } + Websocket.start(); + $('#loading').hide(); }); diff --git a/src/sunstone/public/app/sunstone-config.js b/src/sunstone/public/app/sunstone-config.js index 16ed93bb6d5..4abebd233ea 100644 --- a/src/sunstone/public/app/sunstone-config.js +++ b/src/sunstone/public/app/sunstone-config.js @@ -181,6 +181,9 @@ define(function(require) { }, "isExtendedVmInfo": _config["system_config"] && _config["system_config"]["get_extended_vm_info"] && _config["system_config"]["get_extended_vm_info"] === "true", "isLogEnabled": _config["zone_id"] === _config["id_own_federation"] ? true : false, + "autorefreshWSS": _config["system_config"]["autorefresh_wss"], + "autorefreshIP": _config["system_config"]["autorefresh_ip"], + "autorefreshPort": _config["system_config"]["autorefresh_port"], }; return Config; diff --git a/src/sunstone/public/app/sunstone.js b/src/sunstone/public/app/sunstone.js index 4c9cce30826..3edd425b1fa 100644 --- a/src/sunstone/public/app/sunstone.js +++ b/src/sunstone/public/app/sunstone.js @@ -811,7 +811,7 @@ define(function(require) { return context.data("element"); }; - var _insertPanels = function(tabName, info, contextTabId, context) { + var _insertPanels = function(tabName, info, contextTabId, context, autorefresh=false) { var context = context || $(".sunstone-info", $("#" + tabName)); context.data("element", info[Object.keys(info)[0]]); @@ -885,10 +885,12 @@ define(function(require) { context.html(html); $.each(SunstoneCfg["tabs"][tabName]["panelInstances"], function(panelName, panel) { - panel.setup(context); + if (!autorefresh || panelName == "vm_info_tab"){ + panel.setup(context); - if(isRefresh && prevPanelStates[panelName] && panel.setState){ - panel.setState( prevPanelStates[panelName], context ); + if(isRefresh && prevPanelStates[panelName] && panel.setState){ + panel.setState( prevPanelStates[panelName], context ); + } } }); @@ -914,6 +916,11 @@ define(function(require) { } }; + var _autorefreshVM = function(tabName, info, contextTabId, context) { + _insertPanels(tabName, info, contextTabId, context, true); + }; + + //Runs a predefined action. Wraps the calls to opennebula.js and //can be use to run action depending on conditions and notify them //if desired. Returns 1 if some problem has been detected: i.e @@ -1358,6 +1365,7 @@ define(function(require) { "insertTabs": _insertTabs, "insertPanels": _insertPanels, + "autorefreshVM": _autorefreshVM, "getElementRightInfo": _getElementRightInfo, "showTab": _showTab, diff --git a/src/sunstone/public/app/tabs/vms-tab/panels/info.js b/src/sunstone/public/app/tabs/vms-tab/panels/info.js index eb82445f600..ba4d51a1d50 100644 --- a/src/sunstone/public/app/tabs/vms-tab/panels/info.js +++ b/src/sunstone/public/app/tabs/vms-tab/panels/info.js @@ -27,6 +27,7 @@ define(function(require) { var TemplateTableVcenter = require("utils/panel/template-table"); var OpenNebula = require("opennebula"); var Navigation = require("utils/navigation"); + var Websocket = require("utils/websocket"); /* TEMPLATES @@ -169,5 +170,8 @@ define(function(require) { } TemplateTable.setup(strippedTemplate, RESOURCE, this.element.ID, context, unshownValues, strippedTemplateVcenter); TemplateTableVcenter.setup(strippedTemplateVcenter, RESOURCE, this.element.ID, context, unshownValues, strippedTemplate); + + Websocket.subscribe(this.element.ID); + } }); diff --git a/src/sunstone/public/app/tabs/vms-tab/panels/info/html.hbs b/src/sunstone/public/app/tabs/vms-tab/panels/info/html.hbs index aa02804fff2..bfe04f510db 100644 --- a/src/sunstone/public/app/tabs/vms-tab/panels/info/html.hbs +++ b/src/sunstone/public/app/tabs/vms-tab/panels/info/html.hbs @@ -33,12 +33,12 @@ {{{renameTrHTML}}} {{tr "State"}} - {{stateStr}} + {{stateStr}} {{tr "LCM State"}} - {{lcmStateStr}} + {{lcmStateStr}} diff --git a/src/sunstone/public/app/utils/websocket.js b/src/sunstone/public/app/utils/websocket.js new file mode 100644 index 00000000000..bf08854f6b3 --- /dev/null +++ b/src/sunstone/public/app/utils/websocket.js @@ -0,0 +1,112 @@ +/* -------------------------------------------------------------------------- */ +/* Copyright 2002-2020, OpenNebula Project, OpenNebula Systems */ +/* */ +/* 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. */ +/* -------------------------------------------------------------------------- */ + +define(function (require) { + + var Config = require("sunstone-config"); + var Sunstone = require('sunstone'); + + + // user config + const wss = Config.autorefreshWSS || 'ws'; + const port = Config.autorefreshPort || 9869; + const host = Config.autorefreshIP || '127.0.0.1'; + + var address = wss + "://" + host + ":" + port + "/ws"; + var ws = new WebSocket(address); + + var _start = function () { + ws.addEventListener('open', function (event) { + console.log("Connected to websocket"); + ws.readyState = 1; + // Send CSRF token + var msg = { + "STATE": ws.readyState, + "ACTION": "authenticate", + } + + ws.send(JSON.stringify(msg)); + }); + + // Listen for messages + ws.addEventListener('message', function (event) { + var vm_info = JSON.parse(event.data); + // console.log(vm_info); + var response = { "VM": vm_info.HOOK_MESSAGE.VM }; + var request = { + "request": { + "data": [response.ID], + "method": "show", + "resource": "VM" + } + } + + // update VM + + var TAB_ID = "vms-tab"; + var tab = $('#' + TAB_ID); + Sunstone.getDataTable(TAB_ID).updateElement(request, response); + if (Sunstone.rightInfoVisible(tab) && vm_info.HOOK_MESSAGE.RESOURCE_ID == Sunstone.rightInfoResourceId(tab)) { + Sunstone.autorefreshVM(TAB_ID, response); + } + + if (vm_info.HOOK_MESSAGE.STATE == "DONE"){ + Sunstone.getDataTable(TAB_ID).waitingNodes(); + Sunstone.runAction("VM.list", {force: true}); + } + + }); + + // Close Socket when close browser or tab. + window.onbeforeunload = function () { + _close(); + }; + }; + + var _subscribe = function (vm_id, context) { + var msg = { + "SUBSCRIBE": true, + "VM": vm_id + } + + ws.send(JSON.stringify(msg)); + }; + + var _unsubscribe = function (vm_id) { + var msg = { + "SUBSCRIBE": false, + "VM": vm_id + } + + ws.send(JSON.stringify(msg)); + }; + + var _close = function () { + ws.onclose = function () { }; // disable onclose handler first + ws.close() + }; + + + + var websocket = { + "start": _start, + "subscribe": _subscribe, + "unsubscribe": _unsubscribe, + "close": _close + }; + + return websocket; +}); \ No newline at end of file diff --git a/src/sunstone/sunstone-server.rb b/src/sunstone/sunstone-server.rb index d5ecf2f4a98..caaea7f5eac 100755 --- a/src/sunstone/sunstone-server.rb +++ b/src/sunstone/sunstone-server.rb @@ -123,6 +123,12 @@ require 'SunstoneServer' require 'SunstoneViews' +require 'sinatra-websocket' +require 'eventmachine' +require 'json' +require 'active_support/core_ext/hash' +require 'ffi-rzmq' + begin require "SunstoneWebAuthn" webauthn_avail = true @@ -161,6 +167,7 @@ set :config, $conf set :bind, $conf[:host] set :port, $conf[:port] +set :sockets, [] if (proxy = $conf[:proxy]) ENV['http_proxy'] = proxy @@ -262,6 +269,35 @@ set :erb, :trim => '-' end +#start Autorefresh server + +## 0MQ variables +@context = ZMQ::Context.new(1) +@subscriber = @context.socket(ZMQ::SUB) + +## Subscribe to VM changes +@subscriber.setsockopt(ZMQ::SUBSCRIBE, "EVENT VM") +@subscriber.connect($conf[:zeromq_server]) + +# Create a thread to get ZeroMQ messages +Thread.new do + loop do + key = '' + content = '' + + @subscriber.recv_string(key) + @subscriber.recv_string(content) + + message = Hash.from_xml(Base64.decode64(content)).to_json + + if (key != '') + settings.sockets.each do |client| + client.send(message) + end + end + end +end + $addons = OpenNebulaAddons.new(logger) DEFAULT_TABLE_ORDER = "desc" @@ -461,6 +497,9 @@ def build_session session[:default_view] = $views_config.available_views(session[:user], session[:user_gname]).first end + autorefresh_wss = $conf[:autorefresh_support_wss] + session[:autorefresh_wss] = autorefresh_wss == 'yes'? 'wss' : 'ws' + # end user options # secure cookies @@ -507,7 +546,7 @@ def destroy_session @request_body = request.body.read request.body.rewind - unless %w(/ /login /vnc /spice /version /webauthn_options_for_get).include?(request.path) + unless %w(/ /login /vnc /spice /version /webauthn_options_for_get /ws).include?(request.path) halt [401, "csrftoken"] unless authorized? && valid_csrftoken? end @@ -626,6 +665,27 @@ def destroy_session } end +get '/ws' do + logger.info { 'Incomming WS connection' } + if request.websocket? + request.websocket do |ws| + ws.onopen do + logger.info { "New client registered" } + settings.sockets << ws + end + + ws.onmessage do |msg| + logger.info { "New message received: #{msg}" } + end + + ws.onclose do + logger.info { "Client disconnected." } + settings.sockets.delete(ws) + end + end + end +end + get '/login' do content_type 'text/html', :charset => 'utf-8' if !authorized? diff --git a/src/sunstone/views/index.erb b/src/sunstone/views/index.erb index b001e1b1105..243a29690d1 100644 --- a/src/sunstone/views/index.erb +++ b/src/sunstone/views/index.erb @@ -53,7 +53,11 @@ 'max_upload_file_size' : <%= $conf[:max_upload_file_size] ? $conf[:max_upload_file_size] : "undefined" %>, 'leases' : <%= $conf[:leases] ? $conf[:leases].to_json : "null" %>, 'mapped_ips' : '<%= $conf[:mapped_ips] ? $conf[:mapped_ips] : false %>', - 'get_extended_vm_info': '<%= $conf[:get_extended_vm_info] ? $conf[:get_extended_vm_info] : false %>' + 'get_extended_vm_info': '<%= $conf[:get_extended_vm_info] ? $conf[:get_extended_vm_info] : false %>', + 'autorefresh_wss': '<%= session[:autorefresh_wss] %>', + 'autorefresh_ip': '<%= $conf[:autorefresh_ip] %>', + 'autorefresh_port': '<%= $conf[:port] %>', + }, 'view' : view, 'available_views' : available_views,