diff --git a/.gitignore b/.gitignore index d51208d769..584ea1bedf 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,9 @@ src/sunstone/public/css/app.min.css src/sunstone/public/dist/ src/sunstone/public/locale/languages/*.js src/sunstone/public/package-lock.json +src/sunstone/guac/node_modules +src/sunstone/guac/dist +src/sunstone/guac/package-lock.json .tx/config src/fireedge/node_modules diff --git a/install.sh b/install.sh index 91dc3321e5..d5155b7e10 100755 --- a/install.sh +++ b/install.sh @@ -739,6 +739,7 @@ INSTALL_SUNSTONE_FILES=( INSTALL_SUNSTONE_PUBLIC_MINIFIED_FILES=( SUNSTONE_PUBLIC_JS_FILES:$SUNSTONE_LOCATION/public/dist + SUNSTONE_PUBLIC_JS_GUAC_FILES:$SUNSTONE_LOCATION/guac/dist SUNSTONE_PUBLIC_JS_CONSOLE_FILES:$SUNSTONE_LOCATION/public/dist/console SUNSTONE_PUBLIC_FONT_AWSOME:$SUNSTONE_LOCATION/public/bower_components/fontawesome/web-fonts-with-css/webfonts SUNSTONE_PUBLIC_CSS_FILES:$SUNSTONE_LOCATION/public/css @@ -767,6 +768,7 @@ INSTALL_SUNSTONE_PUBLIC_MINIFIED_FILES=( INSTALL_SUNSTONE_PUBLIC_DEV_DIR=( SUNSTONE_PUBLIC_DEV_DIR:$SUNSTONE_LOCATION + SUNSTONE_GUAC_DEV_DIR:$SUNSTONE_LOCATION ) INSTALL_SUNSTONE_ETC_FILES=( @@ -895,6 +897,7 @@ RUBY_LIB_FILES="src/mad/ruby/ActionManager.rb \ src/vnm_mad/one_vnm.rb \ src/oca/ruby/opennebula.rb \ src/sunstone/OpenNebulaVNC.rb \ + src/sunstone/OpenNebulaGuac.rb \ src/sunstone/OpenNebulaAddons.rb \ src/vmm_mad/remotes/vcenter/vcenter_driver.rb \ src/vmm_mad/remotes/nsx/nsx_driver.rb \ @@ -2373,6 +2376,7 @@ SUNSTONE_FILES="src/sunstone/sunstone-server.rb \ src/sunstone/config.ru" SUNSTONE_BIN_FILES="src/sunstone/bin/sunstone-server \ + src/sunstone/bin/guac-server \ src/sunstone/bin/novnc-server" SUNSTONE_ETC_FILES="src/sunstone/etc/sunstone-server.conf \ @@ -2436,6 +2440,10 @@ SUNSTONE_PUBLIC_JS_FILES="src/sunstone/public/dist/login.js \ src/sunstone/public/dist/main.js.map \ src/sunstone/public/dist/main-dist.js" +SUNSTONE_PUBLIC_JS_GUAC_FILES="src/sunstone/guac/dist/guac.js" + +SUNSTONE_GUAC_DEV_DIR="src/sunstone/guac" + SUNSTONE_PUBLIC_JS_CONSOLE_FILES="src/sunstone/public/dist/console/vnc.js \ src/sunstone/public/dist/console/vnc.js.map \ src/sunstone/public/dist/console/spice.js \ diff --git a/src/sunstone/OpenNebulaGuac.rb b/src/sunstone/OpenNebulaGuac.rb new file mode 100644 index 0000000000..4f2d664f0c --- /dev/null +++ b/src/sunstone/OpenNebulaGuac.rb @@ -0,0 +1,325 @@ +# -------------------------------------------------------------------------- # +# 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. # +#--------------------------------------------------------------------------- # + +# +# This class provides support for launching and stopping a websockify proxy +# + +require 'rubygems' +require 'json' +require 'opennebula' +require 'base64' +require 'openssl' + + +if !ONE_LOCATION + GUAC_LOCK_FILE = "/var/lock/one/.guac.lock" + GUACD_PID_FILE = "/var/run/one/guacd.pid" +else + GUAC_LOCK_FILE= ONE_LOCATION + "/var/.guac.lock" + GUACD_PID_FILE= ONE_LOCATION + "/var/guacd.pid" +end + +GUAC_STATES = [ + #0, #LCM_INIT + #1, #PROLOG + #2, #BOOT + "3", #RUNNING + "4", #MIGRATE + #5, #SAVE_STOP + #6, #SAVE_SUSPEND + #7, #SAVE_MIGRATE + #8, #PROLOG_MIGRATE + #9, #PROLOG_RESUME + #10, #EPILOG_STOP + #11, #EPILOG + "12", #SHUTDOWN + "13", #CANCEL + #14, #FAILURE + #15, #CLEANUP_RESUBMIT + "16", #UNKNOWN + "17", #HOTPLUG + "18", #SHUTDOWN_POWEROFF + #19, #BOOT_UNKNOWN + #20, #BOOT_POWEROFF + #21, #BOOT_SUSPENDED + #22, #BOOT_STOPPED + #23, #CLEANUP_DELETE + "24", #HOTPLUG_SNAPSHOT + "25", #HOTPLUG_NIC + "26", #HOTPLUG_SAVEAS + "27", #HOTPLUG_SAVEAS_POWEROFF + "28", #HOTPLUG_SAVEAS_SUSPENDED + "29", #SHUTDOWN_UNDEPLOY + #30, #EPILOG_UNDEPLOY + #31, #PROLOG_UNDEPLOY + #32, #BOOT_UNDEPLOY + #33, #HOTPLUG_PROLOG_POWEROFF + #34, #HOTPLUG_EPILOG_POWEROFF + #35, #BOOT_MIGRATE + #36, #BOOT_FAILURE + #37, #BOOT_MIGRATE_FAILURE + #38, #PROLOG_MIGRATE_FAILURE + #39, #PROLOG_FAILURE + #40, #EPILOG_FAILURE + #41, #EPILOG_STOP_FAILURE + #42, #EPILOG_UNDEPLOY_FAILURE + #43, #PROLOG_MIGRATE_POWEROFF + #44, #PROLOG_MIGRATE_POWEROFF_FAILURE + #45, #PROLOG_MIGRATE_SUSPEND + #46, #PROLOG_MIGRATE_SUSPEND_FAILURE + #47, #BOOT_UNDEPLOY_FAILURE + #48, #BOOT_STOPPED_FAILURE + #49, #PROLOG_RESUME_FAILURE + #50, #PROLOG_UNDEPLOY_FAILURE + #51, #DISK_SNAPSHOT_POWEROFF + #52, #DISK_SNAPSHOT_REVERT_POWEROFF + #53, #DISK_SNAPSHOT_DELETE_POWEROFF + #54, #DISK_SNAPSHOT_SUSPENDED + #55, #DISK_SNAPSHOT_REVERT_SUSPENDED + #56, #DISK_SNAPSHOT_DELETE_SUSPENDED + "57", #DISK_SNAPSHOT + "58", #DISK_SNAPSHOT_REVERT + #59, #DISK_SNAPSHOT_DELETE + #60, #PROLOG_MIGRATE_UNKNOWN + #61, #PROLOG_MIGRATE_UNKNOWN_FAILURE + "62" #DISK_RESIZE + #63, #DISK_RESIZE_POWEROFF + #64 #DISK_RESIZE_UNDEPLOYED + #65 #HOTPLUG_NIC_POWEROFF +] + +class OpenNebulaGuac + + attr_reader :proxy_port + + def initialize(config, logger, options = {}) + opts={ :json_errors => true }.merge(options) + + @lock_file = GUAC_LOCK_FILE + @guacd_pid_file = GUACD_PID_FILE + + @options = opts + @logger = logger + end + + def proxy(vm_resource, type_connection = "vnc") + # Check configurations and VM attributes + unless is_lockfile? + return error(400, "Guacamole server is not running") + end + + if !is_running? + return error(400, "Guacamole server is not running") + end + + if !is_guacd_running? + return error(400, "Guacamole proxy daemon is not running") + end + + # Check configurations and VM attributes + if !GUAC_STATES.include?(vm_resource['LCM_STATE']) + return error(400, "Wrong state (#{vm_resource['LCM_STATE']}) to open a Guacamole session") + end + + if !(["vnc", "rdp", "ssh"].include?(type_connection.downcase)) + return error(400, "Type connection not supported by Guacamole") + end + + # TODO (* : optional) =============================== + # ======================================================= + # type : telnet + # hostname : vm_resource["TEMPLATE/NIC[1]/IP"] + # port* : 23 by default + # username* : vm_resource['TEMPLATE/CONTEXT/USERNAME'] + # password* : vm_resource['TEMPLATE/CONTEXT/PASSWORD'] + + settings = case type_connection.downcase + when "vnc" then get_config_vnc(vm_resource) + when "rdp" then get_config_rdp(vm_resource) + when "ssh" then get_config_ssh(vm_resource) + else {:error => error(400, "Type connection not supported by Guacamole")} + end + + return settings[:error] if settings.key?(:error) + + data = encrypt_data({ + "connection": { + "type": type_connection.downcase, + "settings": { + "security": "any", + "ignore-cert": "true", + "enable-drive": "true", + "enable-audio": "true", + "create-drive-path": "true", + }.merge!(settings) + } + }) + + [200, { :token => data }.to_json] + end + + def status + if is_running? + STDOUT.puts "Guacamole server is running" + true + else + STDOUT.puts "Guacamole server is NOT running" + false + end + end + + private + + def error(code, msg) + if @options[:json_errors] + return [code,OpenNebula::Error.new(msg).to_json] + else + return [code,msg] + end + end + + def is_running? + if File.exist?(@lock_file) + pid=File.read(@lock_file).strip + + if system("ps #{pid} 1> /dev/null") + return pid.to_i + end + + @logger.info "Deleting stale lock file" + File.delete(@lock_file) + end + + false + end + alias_method :get_guac_pid, :is_running? + + def is_guacd_running? + if File.exist?(@guacd_pid_file) + pid=File.read(@guacd_pid_file).strip + + if system("ps #{pid} 1> /dev/null") + return pid.to_i + end + end + + false + end + alias_method :get_guacd_pid, :is_guacd_running? + + def is_lockfile? + dn = File.dirname(@lock_file) + bn = File.basename(@lock_file) + + # Due to restrictions which doesn't allow to stat the lock file + # when Sunstone is running under Apache with SELinux enabled, + # as a workaround we read content of the whole lock directory. + begin + Dir.entries(dn).include?(bn) + rescue StandardError + false + end + end + +if RUBY_VERSION<'1.9' + def spawn(*args) + fork { + command=args[0..-2] + + # Close stdin and point out and err to log file + $stdout.reopen(GUACAMOLE_LOG, "a") + $stderr.reopen(GUACAMOLE_LOG, "a") + $stdin.close + + # Detach process from the parent + Process.setsid + + exec(*command) + } + end +end + + def get_config_vnc(vm_resource) + # If it is a vCenter VM + if vm_resource['USER_TEMPLATE/HYPERVISOR'] == "vcenter" + if vm_resource['MONITORING/VCENTER_ESX_HOST'] + hostname = vm_resource['MONITORING/VCENTER_ESX_HOST'] + else + return error(400,"Could not determine the vCenter ESX host where the VM is running. + Wait till the VCENTER_ESX_HOST attribute is retrieved once the host has been monitored") + end + else + hostname = vm_resource['/VM/HISTORY_RECORDS/HISTORY[last()]/HOSTNAME'] + end + + { "hostname": "localhost", "port": "5900", "password": nil }.merge({ + "hostname": hostname, + "port": vm_resource['TEMPLATE/GRAPHICS/PORT'], + "password": vm_resource['TEMPLATE/GRAPHICS/PASSWD'] + }) + end + + def get_config_rdp(vm_resource) + hostname = vm_resource["TEMPLATE/NIC[RDP='YES'][1]/IP | TEMPLATE/NIC_ALIAS[RDP='YES'][1]/IP"] + + return { + :error => error(400, "Wrong configuration. Cannot find a NIC with RDP") + } if hostname.nil? + + { "hostname": "localhost", "port": "3389", "username": nil, "password": nil }.merge({ + "hostname": hostname, + "port": vm_resource['TEMPLATE/CONTEXT/RDP_PORT'], + "username": vm_resource['TEMPLATE/CONTEXT/USERNAME'], + "password": vm_resource['TEMPLATE/CONTEXT/PASSWORD'] + }) + end + + def get_config_ssh(vm_resource) + hostname = vm_resource["TEMPLATE/NIC[SSH='YES'][1]/IP | TEMPLATE/NIC_ALIAS[SSH='YES'][1]/IP"] + + return { + :error => error(400, "Wrong configuration. Cannot find a NIC with SSH") + } if hostname.nil? + + { "hostname": "localhost", "port": "22", "username": nil, "password": nil }.merge({ + "hostname": hostname, + "port": vm_resource['TEMPLATE/CONTEXT/SSH_PORT'], + "username": vm_resource['TEMPLATE/CONTEXT/USERNAME'], + "password": vm_resource['TEMPLATE/CONTEXT/PASSWORD'] + }) + end + + def encrypt_data(data) + iv = OpenSSL::Random.random_bytes(16) + key = 'LSIOGCKYLSIOGCKYLSIOGCKYLSIOGCKY' #OpenSSL::Random.random_bytes(32) + + cipher = OpenSSL::Cipher::AES256.new(:CBC) + cipher.encrypt + cipher.key = key + cipher.iv = iv + + crypted = cipher.update(data.to_json) + crypted << cipher.final + token = { + iv: Base64.encode64(iv).strip, + value: Base64.strict_encode64(crypted).strip + } + + return Base64.strict_encode64(token.to_json).encode('utf-8').strip + end +end + diff --git a/src/sunstone/OpenNebulaVNC.rb b/src/sunstone/OpenNebulaVNC.rb index 1e2931ac80..0ce88f9eba 100644 --- a/src/sunstone/OpenNebulaVNC.rb +++ b/src/sunstone/OpenNebulaVNC.rb @@ -285,6 +285,8 @@ class OpenNebulaVNC end delete_token_dir + + STDOUT.puts "VNC proxy stopped" else message="VNC server is not running" @logger.info message diff --git a/src/sunstone/bin/guac-server b/src/sunstone/bin/guac-server new file mode 100755 index 0000000000..7b5f2dacd1 --- /dev/null +++ b/src/sunstone/bin/guac-server @@ -0,0 +1,196 @@ +#!/bin/bash + +# -------------------------------------------------------------------------- # +# 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. # +#--------------------------------------------------------------------------- # + +GUACD_BIN=/usr/local/sbin/guacd + +if [ -z "$ONE_LOCATION" ]; then + GUAC_SERVER_DIR=/usr/lib/one/sunstone/guac + GUAC_LOCK_FILE=/var/lock/one/.guac.lock + GUAC_LOG=/var/log/one/guac.log + GUAC_LOG_ERROR=/var/log/one/guac.error + GUAC_PID=/var/run/one/guac.pid + GUACD_PID=/var/run/one/guacd.pid + SUNSTONE_CONF=/etc/one/sunstone-server.conf +else + GUAC_LOCK_FILE=$ONE_LOCATION/var/.guac.lock + GUAC_LOG=$ONE_LOCATION/var/guac.log + GUAC_LOG_ERROR=$ONE_LOCATION/var/guac.error + GUAC_PID=$ONE_LOCATION/var/guac.pid + GUACD_PID=$ONE_LOCATION/var/guacd.pid + SUNSTONE_CONF=$ONE_LOCATION/etc/sunstone-server.conf +fi + +get_pid() +{ + if [ -f "$1" ]; then + read PID < "$1" + + # if pidfile contains PID and PID is valid + if [ -n "$PID" ] && ps "$PID" > /dev/null 2>&1; then + echo "$PID" + return 0 + fi + fi + + # pidfile/pid not found, or process is dead + return 1 +} + +start_guacd() +{ + if [ ! -f "$GUACD_BIN" ]; then + echo "Cannot find $GUACD_BIN." + return 1 + fi + + PID=`get_pid $GUACD_PID` + if [ $? -eq 0 ]; then + echo "Guacamole proxy daemon already running" + return 1 + fi + + echo -n "Starting guacd: " + $GUACD_BIN -p "$GUACD_PID" > /dev/null 2>&1 + + case "$?" in + 0) + echo "SUCCESS" + ;; + *) + echo "FAIL" + ;; + esac +} + +stop_guacd() +{ + echo -n "Stopping guacd: " + + PID=`get_pid $GUACD_PID` + case "$?" in + 0) + if kill -9 $PID > /dev/null 2>&1 + then + echo "SUCCESS" + return 0 + fi + + echo "FAIL" + return 0 + ;; + *) + echo "SUCCESS (not running)" + return 0 + ;; + esac +} + +setup() +{ + if [ -f $GUAC_LOCK_FILE ]; then + PID=`get_pid $GUAC_PID` + if [ $? -eq 0 ]; then + echo -n "Guacamole Server is still running (PID:$PID). Please " + echo "try 'guac-server stop' first." + exit 1 + fi + echo "Stale .lock detected. Erasing it." + rm $GUAC_LOCK_FILE + fi +} + +start() +{ + ENV_MODE=$(grep ^:env: $SUNSTONE_CONF|awk '{print $2}') + if [ "$ENV_MODE" = "dev" ]; then + GUAC_SERVER=$GUAC_SERVER_DIR/app.js + else + GUAC_SERVER=$GUAC_SERVER_DIR/dist/guac.js + fi + + if [ ! -f "$GUAC_SERVER" ]; then + echo "Cannot find $GUAC_SERVER." + exit 1 + fi + + touch $GUAC_LOCK_FILE + + # Start the guac-server daemon + GUAC_PORT=$(grep ^:guac_port: $SUNSTONE_CONF|awk '{print $2}') + GUACD_PORT=$(grep ^:guacd_port: $SUNSTONE_CONF|awk '{print $2}') + PORT="$GUAC_PORT" GUACD_PORT="$GUACD_PORT" node "$GUAC_SERVER" \ + > $GUAC_LOG 2>$GUAC_LOG_ERROR & + + LASTRC=$? + LASTPID=$! + + if [ $LASTRC -ne 0 ]; then + echo "Error executing guac-server." + echo "Check $GUAC_LOG_ERROR and $GUAC_LOG for more information" + exit 1 + else + echo $LASTPID > $GUAC_PID + fi + + sleep 2 + ps $LASTPID > /dev/null 2>&1 + + if [ $? -ne 0 ]; then + echo "Error executing guac-server." + echo "Check $GUAC_LOG_ERROR and $GUAC_LOG for more information" + exit 1 + fi + + echo "guac-server started" +} + +# +# Function that stops the daemon/service +# +stop() +{ + if [ ! -f $GUAC_PID ]; then + echo "Couldn't find guac-server process pid." + exit 1 + fi + + # Kill the guac-server daemon + kill -9 `cat $GUAC_PID` > /dev/null 2>&1 + + # Remove pid files + rm -f $GUAC_PID > /dev/null 2>&1 + rm -f $GUAC_LOCK_FILE &> /dev/null + + echo "guac-server stopped" +} + +case "$1" in + start) + start_guacd + setup + start + ;; + stop) + stop_guacd + stop + ;; + *) + echo "Usage: guac-server {start|stop}" >&2 + exit 3 + ;; +esac diff --git a/src/sunstone/bin/sunstone-server b/src/sunstone/bin/sunstone-server index f92571920c..c2c7acf211 100755 --- a/src/sunstone/bin/sunstone-server +++ b/src/sunstone/bin/sunstone-server @@ -25,6 +25,7 @@ if [ -z "$ONE_LOCATION" ]; then SUNSTONE_LOG_ERROR=/var/log/one/sunstone.error SUNSTONE_CONF=/etc/one/sunstone-server.conf NOVNC_SERVER=/usr/bin/novnc-server + GUACAMOLE_SERVER=/usr/bin/guac-server else SUNSTONE_PID=$ONE_LOCATION/var/sunstone.pid SUNSTONE_SERVER=$ONE_LOCATION/lib/sunstone/sunstone-server.rb @@ -33,6 +34,7 @@ else SUNSTONE_LOG_ERROR=$ONE_LOCATION/var/sunstone.error SUNSTONE_CONF=$ONE_LOCATION/etc/sunstone-server.conf NOVNC_SERVER=$ONE_LOCATION/bin/novnc-server + GUACAMOLE_SERVER=$ONE_LOCATION/bin/guac-server fi setup() @@ -59,12 +61,15 @@ setup() fi } - start() { - if [ "$1" = "-n" ]; then - novnc=1 - fi + while getopts "ng" opt; do + case "${opt}" in + n) novnc=1 ;; + g) guacamole=1 ;; + *) echo "Invalid option: $OPTARG" 1>&2 ;; + esac + done if [ ! -f "$SUNSTONE_SERVER" ]; then echo "Cannot find $SUNSTONE_SERVER." @@ -80,6 +85,15 @@ start() fi fi + if [ "$guacamole" = "1" ]; then + # Start guacamole server + $GUACAMOLE_SERVER start + + if [ "$?" != "0" ]; then + echo "Could not start Guacamole server" 1>&2 + fi + fi + # Start the sunstone daemon touch $SUNSTONE_LOCK_FILE @@ -87,6 +101,7 @@ start() echo "Error creating lock file" 1>&2 $NOVNC_SERVER stop + $GUACAMOLE_SERVER stop exit 1 fi @@ -97,6 +112,7 @@ start() echo "Error executing sunstone-server." echo "Check $SUNSTONE_LOG_ERROR and $SUNSTONE_LOG for more information" [ "$novnc" = "1" ] && $NOVNC_SERVER stop + [ "$guacamole" = "1" ] && $GUACAMOLE_SERVER stop exit 1 else echo $LASTPID > $SUNSTONE_PID @@ -109,6 +125,7 @@ start() echo "Error executing sunstone-server." echo "Check $SUNSTONE_LOG_ERROR and $SUNSTONE_LOG for more information" [ "$novnc" = "1" ] && $NOVNC_SERVER stop + [ "$guacamole" = "1" ] && $GUACAMOLE_SERVER stop exit 1 fi @@ -120,13 +137,20 @@ start() # stop() { - if [ "$1" = "-n" ]; then - novnc=1 - fi + while getopts "ng" opt; do + case "${opt}" in + n) novnc=1 ;; + g) guacamole=1 ;; + *) echo "Invalid option: $OPTARG" 1>&2 ;; + esac + done # Stop novnc server [ "$novnc" = "1" ] && $NOVNC_SERVER stop + # Stop guacamole server + [ "$guacamole" = "1" ] && $GUACAMOLE_SERVER stop + if [ ! -f $SUNSTONE_PID ]; then echo "Couldn't find sunstone-server process pid." >&2 return 1 @@ -146,16 +170,16 @@ stop() case "$1" in start) setup - start -n + start -ng ;; stop) - stop -n + stop -ng exit $? ;; restart) - stop -n 2> /dev/null + stop -ng 2> /dev/null setup - start -n + start -ng ;; start-sunstone) setup @@ -165,7 +189,7 @@ case "$1" in stop ;; restart-sunstone) - stop -n 2> /dev/null + stop -ng 2> /dev/null setup start ;; diff --git a/src/sunstone/etc/sunstone-server.conf b/src/sunstone/etc/sunstone-server.conf index a998ba20ef..4b6b58752a 100644 --- a/src/sunstone/etc/sunstone-server.conf +++ b/src/sunstone/etc/sunstone-server.conf @@ -158,6 +158,9 @@ :vnc_request_password: false :allow_vnc_federation: no +:guac_port: 29877 +:guacd_port: 4822 + # Login Session Length in seconds, defaults to 1 hour. #:session_expire_time: 3600 diff --git a/src/sunstone/etc/sunstone-views/kvm/admin.yaml b/src/sunstone/etc/sunstone-views/kvm/admin.yaml index f98f477daa..1950b98099 100644 --- a/src/sunstone/etc/sunstone-views/kvm/admin.yaml +++ b/src/sunstone/etc/sunstone-views/kvm/admin.yaml @@ -420,6 +420,9 @@ tabs: VM.menu_labels: true VM.startvnc: true VM.startspice: true + VM.vnc: true + VM.ssh: true + VM.rdp: true VM.save_rdp: true VM.save_virt_viewer: true oneflow-services-tab: diff --git a/src/sunstone/etc/sunstone-views/kvm/cloud.yaml b/src/sunstone/etc/sunstone-views/kvm/cloud.yaml index 96b4e7e625..137a0d0fd3 100644 --- a/src/sunstone/etc/sunstone-views/kvm/cloud.yaml +++ b/src/sunstone/etc/sunstone-views/kvm/cloud.yaml @@ -93,6 +93,9 @@ tabs: VM.unlock: true VM.startvnc: true VM.startspice: true + VM.vnc: true + VM.ssh: true + VM.rdp: true VM.save_rdp: true VM.save_virt_viewer: true dashboard: diff --git a/src/sunstone/etc/sunstone-views/kvm/groupadmin.yaml b/src/sunstone/etc/sunstone-views/kvm/groupadmin.yaml index dae9ab2c7d..d9d5adafdc 100644 --- a/src/sunstone/etc/sunstone-views/kvm/groupadmin.yaml +++ b/src/sunstone/etc/sunstone-views/kvm/groupadmin.yaml @@ -419,6 +419,9 @@ tabs: VM.menu_labels: true VM.startvnc: true VM.startspice: true + VM.vnc: true + VM.ssh: true + VM.rdp: true VM.save_rdp: true VM.save_virt_viewer: true oneflow-services-tab: diff --git a/src/sunstone/etc/sunstone-views/kvm/user.yaml b/src/sunstone/etc/sunstone-views/kvm/user.yaml index dea34db593..20f95e548d 100644 --- a/src/sunstone/etc/sunstone-views/kvm/user.yaml +++ b/src/sunstone/etc/sunstone-views/kvm/user.yaml @@ -410,10 +410,13 @@ tabs: VM.unlock: true VM.edit_labels: true VM.menu_labels: true - VM.save_rdp: true - VM.save_virt_viewer: true VM.startvnc: true VM.startspice: true + VM.vnc: true + VM.ssh: true + VM.rdp: true + VM.save_rdp: true + VM.save_virt_viewer: true oneflow-services-tab: panel_tabs: service_info_tab: true diff --git a/src/sunstone/etc/sunstone-views/mixed/admin.yaml b/src/sunstone/etc/sunstone-views/mixed/admin.yaml index d610da0d7a..2592e8f937 100644 --- a/src/sunstone/etc/sunstone-views/mixed/admin.yaml +++ b/src/sunstone/etc/sunstone-views/mixed/admin.yaml @@ -420,6 +420,9 @@ tabs: VM.menu_labels: true VM.startvnc: true VM.startspice: true + VM.vnc: true + VM.ssh: true + VM.rdp: true VM.save_rdp: true VM.save_virt_viewer: true oneflow-services-tab: diff --git a/src/sunstone/etc/sunstone-views/mixed/cloud.yaml b/src/sunstone/etc/sunstone-views/mixed/cloud.yaml index 43ab48a9a9..6eb65cc817 100644 --- a/src/sunstone/etc/sunstone-views/mixed/cloud.yaml +++ b/src/sunstone/etc/sunstone-views/mixed/cloud.yaml @@ -93,6 +93,9 @@ tabs: VM.unlock: true VM.startvnc: true VM.startspice: true + VM.vnc: true + VM.ssh: true + VM.rdp: true VM.save_rdp: true VM.save_virt_viewer: true dashboard: diff --git a/src/sunstone/etc/sunstone-views/mixed/groupadmin.yaml b/src/sunstone/etc/sunstone-views/mixed/groupadmin.yaml index 2856a0b10c..e24f3a5bc2 100644 --- a/src/sunstone/etc/sunstone-views/mixed/groupadmin.yaml +++ b/src/sunstone/etc/sunstone-views/mixed/groupadmin.yaml @@ -419,6 +419,9 @@ tabs: VM.menu_labels: true VM.startvnc: true VM.startspice: true + VM.vnc: true + VM.ssh: true + VM.rdp: true VM.save_rdp: true VM.save_virt_viewer: true oneflow-services-tab: diff --git a/src/sunstone/etc/sunstone-views/mixed/user.yaml b/src/sunstone/etc/sunstone-views/mixed/user.yaml index 078f92731d..b4f3967175 100644 --- a/src/sunstone/etc/sunstone-views/mixed/user.yaml +++ b/src/sunstone/etc/sunstone-views/mixed/user.yaml @@ -412,6 +412,9 @@ tabs: VM.menu_labels: true VM.startvnc: true VM.startspice: true + VM.vnc: true + VM.ssh: true + VM.rdp: true VM.save_rdp: true VM.save_virt_viewer: true oneflow-services-tab: diff --git a/src/sunstone/etc/sunstone-views/vcenter/admin.yaml b/src/sunstone/etc/sunstone-views/vcenter/admin.yaml index cafd69e7f5..d8530c0976 100644 --- a/src/sunstone/etc/sunstone-views/vcenter/admin.yaml +++ b/src/sunstone/etc/sunstone-views/vcenter/admin.yaml @@ -418,6 +418,9 @@ tabs: VM.menu_labels: true VM.startvnc: true VM.startspice: true + VM.vnc: true + VM.ssh: true + VM.rdp: true VM.save_rdp: true VM.save_virt_viewer: true oneflow-services-tab: diff --git a/src/sunstone/etc/sunstone-views/vcenter/cloud.yaml b/src/sunstone/etc/sunstone-views/vcenter/cloud.yaml index 8ebe4849fa..50d0353ade 100644 --- a/src/sunstone/etc/sunstone-views/vcenter/cloud.yaml +++ b/src/sunstone/etc/sunstone-views/vcenter/cloud.yaml @@ -94,6 +94,9 @@ tabs: VM.unlock: true VM.startvnc: true VM.startspice: true + VM.vnc: true + VM.ssh: true + VM.rdp: true VM.save_rdp: true VM.save_virt_viewer: true dashboard: diff --git a/src/sunstone/etc/sunstone-views/vcenter/groupadmin.yaml b/src/sunstone/etc/sunstone-views/vcenter/groupadmin.yaml index e8e0cc9ee4..5e5a8421a2 100644 --- a/src/sunstone/etc/sunstone-views/vcenter/groupadmin.yaml +++ b/src/sunstone/etc/sunstone-views/vcenter/groupadmin.yaml @@ -419,6 +419,9 @@ tabs: VM.menu_labels: true VM.startvnc: true VM.startspice: true + VM.vnc: true + VM.ssh: true + VM.rdp: true VM.save_rdp: true VM.save_virt_viewer: true oneflow-services-tab: diff --git a/src/sunstone/etc/sunstone-views/vcenter/user.yaml b/src/sunstone/etc/sunstone-views/vcenter/user.yaml index 078f92731d..b4f3967175 100644 --- a/src/sunstone/etc/sunstone-views/vcenter/user.yaml +++ b/src/sunstone/etc/sunstone-views/vcenter/user.yaml @@ -412,6 +412,9 @@ tabs: VM.menu_labels: true VM.startvnc: true VM.startspice: true + VM.vnc: true + VM.ssh: true + VM.rdp: true VM.save_rdp: true VM.save_virt_viewer: true oneflow-services-tab: diff --git a/src/sunstone/guac/app.js b/src/sunstone/guac/app.js new file mode 100644 index 0000000000..6ebf6e223b --- /dev/null +++ b/src/sunstone/guac/app.js @@ -0,0 +1,35 @@ +// Guacamole Client + +//// Application Variables //// +var helmet = require('helmet'); +var app = require('express')(); +var http = require('http').Server(app); +var GuacamoleLite = require('guacamole-lite'); + +var { clientOptions, clientCallbacks } = require('./options'); + +/** + * CONSTANTS + * */ +GUAC_PORT = process.env.PORT || 29877 +GUACD_PORT = process.env.GUACD || 4822 + +/** + * Spinup the Guac websocket proxy on port 29877 if guacd is running + * */ +var guacServer = new GuacamoleLite( + { server: http, path: '/guacamole' }, + { host: '127.0.0.1', port: GUACD_PORT }, + clientOptions, + clientCallbacks +); + +/** + * Helmet helps you secure your Express apps + * */ +app.use(helmet()); + +// Spin up application on port 29877 +http.listen(GUAC_PORT, function(){ + console.log('listening on *:' + GUAC_PORT); +}); diff --git a/src/sunstone/guac/options.js b/src/sunstone/guac/options.js new file mode 100644 index 0000000000..f92af59f1f --- /dev/null +++ b/src/sunstone/guac/options.js @@ -0,0 +1,60 @@ +/** + * CLIENT OPTIONS + */ +var clientOptions = { + crypt: { + cypher: 'AES-256-CBC', + key: 'LSIOGCKYLSIOGCKYLSIOGCKYLSIOGCKY' + }, + allowedUnencryptedConnectionSettings: { + rdp: [ + 'width', + 'height', + 'dpi' + ], + vnc: [ + 'width', + 'height', + 'dpi' + ], + ssh: [ + 'color-scheme', + 'font-name', + 'font-size', + 'width', + 'height', + 'dpi' + ], + telnet: [ + 'color-scheme', + 'font-name', + 'font-size', + 'width', + 'height', + 'dpi' + ] + }, + log: { verbose: false } +}; + +/** + * CALLBACKS + */ +var callbacks = { + processConnectionSettings: function (settings, callback) { + if (settings.expiration && settings.expiration < Date.now()) { + return callback(new Error('Token expired')); + } + + //settings.connection['drive-path'] = '/tmp/guacamole_' + //settings.connection['enable-drive'] = false + //settings.connection['create-drive-path'] = false + + callback(null, settings); + } +}; + +module.exports = { + clientOptions, + callbacks +} diff --git a/src/sunstone/guac/package.json b/src/sunstone/guac/package.json new file mode 100644 index 0000000000..2e8c0330ca --- /dev/null +++ b/src/sunstone/guac/package.json @@ -0,0 +1,21 @@ +{ + "name": "one-guac", + "version": "1.0.0", + "description": "OpenNebula Guacamole Client", + "main": "app.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "webpack --mode=production app.js" + }, + "author": "Sergio Betanzos", + "license": "ISC", + "dependencies": { + "express": "^4.17.1", + "guacamole-lite": "^0.6.2", + "helmet": "^3.23.1" + }, + "devDependencies": { + "webpack": "^4.43.0", + "webpack-cli": "^3.3.12" + } +} diff --git a/src/sunstone/guac/webpack.config.js b/src/sunstone/guac/webpack.config.js new file mode 100644 index 0000000000..ca9e7ccc9f --- /dev/null +++ b/src/sunstone/guac/webpack.config.js @@ -0,0 +1,13 @@ +var path = require('path'); + +module.exports = { + entry: './app.js', + target: 'node', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'guac.js' + }, + stats: { + warningsFilter: /^(?!CriticalDependenciesWarning$)/ + } +}; \ No newline at end of file diff --git a/src/sunstone/models/SunstoneServer.rb b/src/sunstone/models/SunstoneServer.rb index 9b2b5309f5..232e9f4873 100644 --- a/src/sunstone/models/SunstoneServer.rb +++ b/src/sunstone/models/SunstoneServer.rb @@ -20,6 +20,7 @@ require 'OpenNebulaJSON' include OpenNebulaJSON require 'OpenNebulaVNC' +require 'OpenNebulaGuac' require 'OpenNebulaAddons' require 'OpenNebulaJSON/JSONUtils' #include JSONUtils @@ -301,6 +302,34 @@ class SunstoneServer < CloudServer return vnc.proxy(resource) end + ######################################################################## + # Guacamole + ######################################################################## + def startguac(id, type_connection, guac) + resource = retrieve_resource("vm", id) + if OpenNebula.is_error?(resource) + return [404, resource.to_json] + end + + client = @client + vm_pool = VirtualMachinePool.new(client, -1) + user_pool = UserPool.new(client) + + rc = user_pool.info + if OpenNebula.is_error?(rc) + puts rc.message + exit -1 + end + + rc = vm_pool.info + if OpenNebula.is_error?(rc) + puts rc.message + exit -1 + end + + return guac.proxy(resource, type_connection) + end + ######################################################################## # Accounting & Monitoring ######################################################################## diff --git a/src/sunstone/public/app/main.js b/src/sunstone/public/app/main.js index 4469292ea4..75b934c264 100644 --- a/src/sunstone/public/app/main.js +++ b/src/sunstone/public/app/main.js @@ -29,6 +29,9 @@ require.config({ /** Wickedpicker */ "wickedpicker": "../bower_components/wickedpicker/dist/wickedpicker.min", + /** Guacamole-common-js */ + "guacamole-common-js": "../bower_components/guacamole-common-js/dist/guacamole-common.min", + /* DataTables */ "datatables.net": "../bower_components/datatables/media/js/jquery.dataTables", "datatables.foundation": "../bower_components/datatables/media/js/dataTables.foundation", diff --git a/src/sunstone/public/app/opennebula/vm.js b/src/sunstone/public/app/opennebula/vm.js index 28a1f3ced6..7dea17b663 100644 --- a/src/sunstone/public/app/opennebula/vm.js +++ b/src/sunstone/public/app/opennebula/vm.js @@ -512,6 +512,27 @@ define(function(require) { } }); }, + "guac" : function(params) { + var callback = params.success; + var callback_error = params.error; + var id = params.data.id; + var typeConnection = params.data.extra_param; + var resource = RESOURCE; + + var request = OpenNebulaHelper.request(resource, null, params.data); + $.ajax({ + url: "vm/" + id + "/guac/" + typeConnection, + type: "POST", + dataType: "json", + success: function(response) { + return callback ? callback(request, response) : null; + }, + error: function(response) { + return callback_error ? + callback_error(request, OpenNebulaError(response)) : null; + } + }); + }, "append": function(params) { var action_obj = {"template_raw" : params.data.extra_param, append : true}; OpenNebulaAction.simple_action(params, RESOURCE, "update", action_obj); @@ -666,13 +687,14 @@ define(function(require) { "isDiskGraphsSupported": isDiskGraphsSupported, "isNICAttachSupported": isNICAttachSupported, "isVNCSupported": isVNCSupported, - "isRDPSupported": isRDPSupported, + "isConnectionSupported": isConnectionSupported, "isSPICESupported": isSPICESupported, "isWFileSupported": isWFileSupported, "buttonVnc": buttonVnc, "buttonSpice": buttonSpice, "buttonWFile": buttonWFile, - "buttonRDP": buttonRDP, + "dropdownRDP": dropdownRDP, + "buttonSSH": buttonSSH, "getName": function(id){ return OpenNebulaAction.getName(id, RESOURCE); } @@ -925,7 +947,7 @@ define(function(require) { // returns true if the vnc button should be enabled function isVNCSupported(element) { - return (Config.isTabActionEnabled("vms-tab", "VM.startvnc") && graphicSupported(element, "vnc")) + return (Config.isTabActionEnabled("vms-tab", "VM.vnc") && graphicSupported(element, "vnc")) ? true : false; } @@ -948,34 +970,43 @@ define(function(require) { : false; } - // returns true if the RDP button should be enabled - function isRDPSupported(element) { - var hasRdp = false; + /** + * returns NIC object if the SSH/RDP should be enabled + * @param {Object} element + * @param {String} typeConnection + */ + function isConnectionSupported(element, typeConnection) { + var isEnabled = false; if ( - Config.isTabActionEnabled("vms-tab", "VM.save_rdp") && + $.inArray(String(typeConnection).toLowerCase(), ['rdp', 'ssh']) > -1 && + Config.isTabActionEnabled("vms-tab", "VM."+ String(typeConnection).toLowerCase()) && element && element.TEMPLATE && element.TEMPLATE.GRAPHICS && element.LCM_STATE ) { var template = element.TEMPLATE; var state = parseInt(element.LCM_STATE); - if ($.inArray(state, RDP_STATES) != -1 && template.NIC) { - hasRdp = hasRDP(template.NIC); + if ($.inArray(state, RDP_STATES) > -1 && template.NIC) { + isEnabled = hasConnection(template.NIC, typeConnection); - if (!hasRdp && template.NIC_ALIAS) { - hasRdp = hasRDP(template.NIC_ALIAS); + if (!isEnabled && template.NIC_ALIAS) { + isEnabled = hasConnection(template.NIC_ALIAS, typeConnection); } } } - return hasRdp; + return isEnabled; } - function hasRDP(nics) { + function hasConnection(nics, typeConnection) { + typeConnection = String(typeConnection).toUpperCase(); var activated = false; nics = Array.isArray(nics) ? nics : [nics]; $.each(nics, function(_, nic) { - if (nic.RDP && String(nic.RDP).toLowerCase() === "yes") { + if ( + nic[typeConnection] && + String(nic[typeConnection]).toLowerCase() === "yes" + ) { activated = nic; } }); @@ -997,23 +1028,48 @@ define(function(require) { } function buttonVnc(id = "") { - return ''; } + function buttonSSH(id = "") { + return ''; + } + function buttonSpice(id = "") { - return ''; } function buttonWFile(id = "", data = {}) { - return ''; } - function buttonRDP(ip = "", vm = {}) { - var username, password; + function dropdownRDP(id = "", ip = "", vm = {}) { + return ''; + } + + function buttonRDP(id = "") { + var styleIcon = 'style="width: 2em;text-align: center;"'; + return ' \ + ' + Locale.tr("HTML") + ''; + } + + function buttonSaveRDP(ip = "", vm = {}) { + var username, password, styleIcon = 'style="width: 2em;text-align: center;"'; if (vm && vm.TEMPLATE && vm.TEMPLATE.CONTEXT) { var context = vm.TEMPLATE.CONTEXT; @@ -1023,13 +1079,14 @@ define(function(require) { (propUpperCase === "PASSWORD") && (password = context[prop]); } } - var button = ' + + + + +
+ diff --git a/src/sunstone/public/app/tabs/vms-tab/hooks/state.js b/src/sunstone/public/app/tabs/vms-tab/hooks/state.js index 313f48465d..ec5003d072 100644 --- a/src/sunstone/public/app/tabs/vms-tab/hooks/state.js +++ b/src/sunstone/public/app/tabs/vms-tab/hooks/state.js @@ -41,13 +41,20 @@ define(function(require) { StateActions.disableAllStateActions(); StateActions.enableStateActions(element.STATE, element.LCM_STATE); - var isVNCSupported = OpenNebulaVM.isVNCSupported(element), - isSPICESupported = OpenNebulaVM.isSPICESupported(element), - isWFileSupported = OpenNebulaVM.isWFileSupported(element), - isRDPSupported = OpenNebulaVM.isRDPSupported(element); + var isVNCSupported = Boolean(OpenNebulaVM.isVNCSupported(element)), + isSPICESupported = Boolean(OpenNebulaVM.isSPICESupported(element)), + isWFileSupported = Boolean(OpenNebulaVM.isWFileSupported(element)), + isRDPSupported = Boolean(OpenNebulaVM.isConnectionSupported(element, 'rdp')), + isSSHSupported = Boolean(OpenNebulaVM.isConnectionSupported(element, 'ssh')); // All remote buttons are disabled - var allDisabled = (!isVNCSupported && !isSPICESupported && !isWFileSupported && !isRDPSupported); + var allDisabled = ( + !isVNCSupported && + !isSPICESupported && + !isWFileSupported && + !isRDPSupported && + !isSSHSupported + ); $("#vmsremote_buttons").toggle(!allDisabled); if (isVNCSupported) { @@ -64,10 +71,13 @@ define(function(require) { } // Show / hide virt-viewer button - $(".vv-sunstone-info").toggle(Boolean(isWFileSupported)); + $(".vv-sunstone-info").toggle(!!isWFileSupported); // Show / hide rdp button - $(".rdp-sunstone-info").toggle(Boolean(isRDPSupported)); + $(".rdp-sunstone-info").toggle(!!isRDPSupported); + + // Show / hide ssh button + $(".ssh-sunstone-info").toggle(!!isSSHSupported); if(config && config["system_config"] && diff --git a/src/sunstone/public/app/tabs/vms-tab/utils/datatable-common.js b/src/sunstone/public/app/tabs/vms-tab/utils/datatable-common.js index 4796bbcaea..6dca1b1ba7 100644 --- a/src/sunstone/public/app/tabs/vms-tab/utils/datatable-common.js +++ b/src/sunstone/public/app/tabs/vms-tab/utils/datatable-common.js @@ -218,9 +218,13 @@ define(function(require) { actions = ''; } - // RDP icon - var rdp = OpenNebulaVM.isRDPSupported(element); - rdp && (actions += OpenNebulaVM.buttonRDP(rdp.IP, element)); + // RDP icons + var rdp = OpenNebulaVM.isConnectionSupported(element, 'rdp'); + rdp && (actions += OpenNebulaVM.dropdownRDP(element.ID, rdp.IP, element)); + + // SSH icon + var ssh = OpenNebulaVM.isConnectionSupported(element, 'ssh'); + ssh && (actions += OpenNebulaVM.buttonSSH(element.ID)); var cpuMonitoring = 0, memoryMonitoring = 0; if (element.MONITORING) { @@ -275,7 +279,7 @@ define(function(require) { hostname, OpenNebulaVM.ipsDropdown(element), Humanize.prettyTimeDatatable(element.STIME), - actions && "
"+actions+"
", + actions && "
"+actions+"
", TemplateUtils.htmlEncode(TemplateUtils.templateToString(element)), (LabelsUtils.labelsStr(element[TEMPLATE_ATTR])||''), btoa(unescape(encodeURIComponent(JSON.stringify(search)))), diff --git a/src/sunstone/public/app/utils/gclient.js b/src/sunstone/public/app/utils/gclient.js new file mode 100644 index 0000000000..938b604e95 --- /dev/null +++ b/src/sunstone/public/app/utils/gclient.js @@ -0,0 +1,243 @@ +/* -------------------------------------------------------------------------- */ +/* 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) { + /** + * DEPENDENCIES + */ + require("guacamole-common-js") + var Config = require("sunstone-config"); + var Notifier = require("utils/notifier"); + var Locale = require("utils/locale"); + + /** + * CONSTRUCTOR + */ + function GClient() { + this._display = null; + this._client = null; + this._mouse = null; + this._keyboard = null; + this._clientErrorText = Locale.tr("The OpenNebula service for remote console is not running, please contact your administrator.") + + return this; + }; + + GClient.prototype = { + "connect": connect, + "mouse": mouse, + "keyboard": keyboard, + "snapshot": getCanvas, + "disconnect": disconnect, + }; + + return GClient; + + function setStatus(state = "") { + $('#guacamole-state').text(state).animate(); + } + + function setLoading(isLoading = false) { + $('#guacamole-loading')[isLoading ? 'fadeIn' : 'fadeOut']('fast'); + } + + function connect(response) { + var that = this; + + if (!response.token) { + Notifier.notifyError(this._clientErrorText) + return null; + } + else setLoading(true); + + var port = Config.guacPort || '29877'; + var host = window.location.hostname || 'localhost'; + var wsprotocol = (window.location.protocol == 'https:') ? 'wss:' : 'ws:'; + + var tunnel = new Guacamole.WebSocketTunnel(wsprotocol + '//' + host + ':' + port + '/guacamole') + var guac = this._client = new Guacamole.Client(tunnel); + + // Client display + this._display = $("#guacamole-display"); + this._display.html(this._client.getDisplay().getElement()); + + // client error handler + guac.onerror = function() { + Notifier.notifyError(that._clientErrorText) + }; + + // websoket error handler + tunnel.onerror = function() { + disconnect(); + Notifier.notifyError(that._clientErrorText) + }; + + guac.onstatechange = function(state) { + switch (state) { + case 0: + setStatus("Client IDLE"); + setLoading(true); + break; + case 1: + setStatus("Client CONNECTING"); + setLoading(true); + break; + case 2: + setStatus("Client WAITING"); + setLoading(true); + break; + case 3: + setStatus("Client CONNECTED"); + setLoading(false); + setTimeout(function() { + rescale(that); + guac.getDisplay().showCursor(false); + }, 100); + break; + case 4: + setStatus("Client DISCONNECTING"); + setLoading(true); + break; + case 5: + setStatus("Client DISCONNECTED"); + setLoading(false); + break; + default: + setStatus("Client ERROR"); + setLoading(false); + break; + } + } + + // Connect + var params = [ + 'token=' + response.token, + 'width=' + this._display.width() + ]; + + try { + guac.connect(params.join('&')); + } catch (error) { + console.log(error) + } + + // Disconnect on close + window.onunload = function () { + disconnect(); + }; + + // Scale display when window resize + $(window).resize(function () { + rescale(that); + }); + } + + /* + GuacamoleWrapper.mouse + - handles mouse interaction + */ + function mouse(enable = true) { + var that = this; + if (!this._client) return; + + if (enable) { + this._mouse = new Guacamole.Mouse(this._client.getDisplay().getElement()); + + // apply sendState function + this._mouse.onmousedown = + this._mouse.onmouseup = function(mouseState) { + that._client.sendMouseState(mouseState); + }; + + this._mouse.onmousemove = function(mouseState) { + mouseState.y = mouseState.y / that._client.getDisplay().getScale(); + mouseState.x = mouseState.x / that._client.getDisplay().getScale(); + that._client.sendMouseState(mouseState); + }; + } + } + + /* + GuacamoleWrapper.keyboard + - handles keyboard interaction + */ + function keyboard(enable = true) { + var that = this; + if (!this._client) return; + + if (enable) { + this._keyboard = new Guacamole.Keyboard(document); + this._keyboard.onkeydown = function(keysym) { + that._client.sendKeyEvent(1, keysym); + } + this._keyboard.onkeyup = function(keysym) { + that._client.sendKeyEvent(0, keysym); + } + } else { + this._keyboard.onkeydown = null; + this._keyboard.onkeyup = null; + this._keyboard = null; + } + } + + /* + GuacamoleWrapper.getCanvas + - shortcut for returning default guac layer (active tunnel viewport) + */ + function getCanvas() { + return (this._client) ? this._client.getDisplay().getDefaultLayer().getCanvas() : false; + } + + function disconnect() { + if (this._client) { + this._client.disconnect(); + this._scale = 1; + $(window).off('resize'); + setStatus() + } + } + + /** + * Updates the scale of the attached Guacamole.Client based on current window + * size and "auto-fit" setting. + */ + function rescale(thisContext) { + var gclientDisplay = thisContext._client.getDisplay(); + + //Get screen resolution. + var origHeigth = Math.max(gclientDisplay.getHeight(), 1); + var origWidth = Math.max(gclientDisplay.getWidth(), 1); + + var htmlWidth = thisContext._display.width(); + var htmlHeigth = thisContext._display.height(); + + var xscale = htmlWidth / origWidth; + var yscale = htmlHeigth / origHeigth; + + // This is done to handle both X and Y axis slacing + var scale = Math.min(yscale, xscale); + + // Limit to 1 + scale = Math.min(scale, 1); + + if (scale !== 0) { + gclientDisplay.scale(scale); + + // Set minimum height container display + thisContext._display.css('min-height', gclientDisplay.getHeight()); + } + } +}); diff --git a/src/sunstone/public/app/utils/nics-section.js b/src/sunstone/public/app/utils/nics-section.js index 2919a5f156..ff8ff5ebc6 100644 --- a/src/sunstone/public/app/utils/nics-section.js +++ b/src/sunstone/public/app/utils/nics-section.js @@ -297,6 +297,7 @@ define(function(require) { var displayType = true; var displaySelection = true; var displayRDP = true; + var displaySSH = true; if( template_json && template_json.VMTEMPLATE && @@ -306,9 +307,11 @@ define(function(require) { var templateType = template_json.VMTEMPLATE.TEMPLATE.SUNSTONE.NETWORK_ALIAS; var templateSelection = template_json.VMTEMPLATE.TEMPLATE.SUNSTONE.NETWORK_AUTO; var templateRDP = template_json.VMTEMPLATE.TEMPLATE.SUNSTONE.NETWORK_RDP; + var templateSSH = template_json.VMTEMPLATE.TEMPLATE.SUNSTONE.NETWORK_SSH; displayType = templateType && templateType.toUpperCase()==='NO'? false : true; displaySelection = templateSelection && templateSelection.toUpperCase()==='NO'? false : true; displayRDP = templateRDP && templateRDP.toUpperCase()==='NO'? false : true; + displaySSH = templateSSH && templateSSH.toUpperCase()==='NO'? false : true; } var dd_context = $(TemplateDD({ vnetsTableHTML: vnetsTable.dataTableHTML, @@ -318,7 +321,8 @@ define(function(require) { options: options, displayType: displayType, displaySelection: displaySelection, - displayRDP: displayRDP + displayRDP: displayRDP, + displaySSH: displaySSH, })).appendTo(context); dd_context["nic_id"] = nicId; @@ -468,7 +472,13 @@ define(function(require) { $("input#provision_accordion_dd_" + provision_nic_accordion_dd_id + "_rdp", dd_context).on("change", function() { const isRDPActivated = $(this).prop('checked'); const idAccordion = "#provision_accordion_dd_" + dd_context["dd_id"]; - _hide_rdp(idAccordion, isRDPActivated, context); + _hide_connection(idAccordion, isRDPActivated, context, 'rdp'); + }); + + $("input#provision_accordion_dd_" + provision_nic_accordion_dd_id + "_ssh", dd_context).on("change", function() { + const isSSHActivated = $(this).prop('checked'); + const idAccordion = "#provision_accordion_dd_" + dd_context["dd_id"]; + _hide_connection(idAccordion, isSSHActivated, context, 'ssh'); }); if ( options.nic && options.nic["NETWORK_MODE"] && options.nic["NETWORK_MODE"] === "auto" ) { @@ -535,6 +545,17 @@ define(function(require) { $("input#provision_accordion_dd_" + provision_nic_accordion_dd_id + "_rdp", context).prop("checked", isRDPActivated); _enableRDP("#provision_accordion_dd_" + provision_nic_accordion_dd_id, context) + // fill ssh connection + const isSSHActivated = ( + options.nic && + options.nic["SSH"] && + options.nic["SSH"] === "YES" && + $("fieldset#ssh_connection input:not(#provision_accordion_dd_" + provision_nic_accordion_dd_id + "_ssh):checked", context).length === 0 + ) ? true : false; + + $("input#provision_accordion_dd_" + provision_nic_accordion_dd_id + "_ssh", context).prop("checked", isSSHActivated); + _enableSSH("#provision_accordion_dd_" + provision_nic_accordion_dd_id, context) + provision_nic_accordion_dd_id += 1; vnetsTable.initialize(); @@ -584,6 +605,7 @@ define(function(require) { nicId --; _enableRDP("#provision_accordion_dd_"+dd_context["nic_id"], context) + _enableSSH("#provision_accordion_dd_"+dd_context["nic_id"], context) return false; }); @@ -637,6 +659,7 @@ define(function(require) { nicId ++; _enableRDP("#provision_accordion_dd_" + provision_nic_accordion_dd_id, context) + _enableSSH("#provision_accordion_dd_" + provision_nic_accordion_dd_id, context) }); if (options.click_add_button == true){ @@ -704,9 +727,11 @@ define(function(require) { }); } - function _hide_rdp(idAccordion, isRDPActivated, context) { - $(".accordion-item > div:not(" + idAccordion + ") fieldset#rdp_connection", context).each(function() { - if (isRDPActivated) { + function _hide_connection(idAccordion, isActivated, context, typeConnection) { + fieldsetSelector = "fieldset#" + typeConnection + "_connection"; + + $(".accordion-item > div:not(" + idAccordion + ") " + fieldsetSelector, context).each(function() { + if (isActivated) { $(this).hide(); } else { $(this).show(); @@ -720,4 +745,11 @@ define(function(require) { if (canRDP) $("fieldset#rdp_connection", context).has("input:not(:checked)").show(); else $("fieldset#rdp_connection", context).has("input:not(:checked)").hide(); } + + function _enableSSH(idAccordion, context) { + const canSSH = $("fieldset#ssh_connection input[type='checkbox']:not(" + idAccordion + "_ssh):checked", context).length === 0; + + if (canSSH) $("fieldset#ssh_connection", context).has("input:not(:checked)").show(); + else $("fieldset#ssh_connection", context).has("input:not(:checked)").hide(); + } }); diff --git a/src/sunstone/public/app/utils/nics-section/dd.hbs b/src/sunstone/public/app/utils/nics-section/dd.hbs index 8ede64b398..27c53cd5d1 100644 --- a/src/sunstone/public/app/utils/nics-section/dd.hbs +++ b/src/sunstone/public/app/utils/nics-section/dd.hbs @@ -23,61 +23,79 @@
-
- {{#if displayType}} -
- {{tr "Interface type"}} -
- - -
- -    - - {{tr "NIC has alias"}} - - -
- {{/if}} - {{#if displaySelection}} -
- {{tr "Network selection"}} -
-
- - -
- -
-
- {{/if}} - {{#if displayRDP}} -
- {{tr "RDP connection"}} -
-
- - -
- -
-
- {{/if}} + {{#if displayType}} +
+ {{tr "Interface type"}} +
+ + +
+ +    + + {{tr "NIC has alias"}} + + +
+ {{/if}} + {{#if displaySelection}} +
+ {{tr "Network selection"}} +
+
+ + +
+ +
+
+ {{/if}} +
+
+ {{#if displayRDP}} +
+ {{tr "RDP connection"}} +
+
+ + +
+ +
+
+ {{/if}} + {{#if displaySSH}} +
+ {{tr "SSH connection"}} +
+
+ + +
+ +
+
+ {{/if}}
diff --git a/src/sunstone/public/bower.json b/src/sunstone/public/bower.json index 0475fd5da2..da165fa698 100644 --- a/src/sunstone/public/bower.json +++ b/src/sunstone/public/bower.json @@ -19,6 +19,7 @@ "sprintf": "~1.0.3", "jquery-ui": "~1.12.1", "wickedpicker": "https://github.com/OpenNebula/sunstone-deps.git#9398b3f", + "guacamole-common-js": "https://github.com/OpenNebula/sunstone-deps.git#1633556e63", "webauthn-json": "https://registry.npmjs.org/@github/webauthn-json/-/webauthn-json-0.4.1.tgz" }, "authors": [ diff --git a/src/sunstone/public/build.sh b/src/sunstone/public/build.sh index 4cafe254ae..1a27b92f81 100755 --- a/src/sunstone/public/build.sh +++ b/src/sunstone/public/build.sh @@ -16,6 +16,7 @@ usage() { clean() { rm -rf dist node_modules bower_components + rm -rf ../guac/dist ../guac/node_modules } dependencies() { @@ -46,6 +47,13 @@ install_enterprise_patch() { done } +install_guac() { + GUAC_DIR="../guac" + + npm i --prefix $GUAC_DIR + npm run build --prefix $GUAC_DIR +} + install_patch() { npm install @@ -78,6 +86,7 @@ install_patch() { mv ./main.js dist/main.js fi + install_guac } #------------------------------------------------------------------------------- diff --git a/src/sunstone/public/scss/_layout.scss b/src/sunstone/public/scss/_layout.scss index 9edb9e2248..c2fa9579c4 100644 --- a/src/sunstone/public/scss/_layout.scss +++ b/src/sunstone/public/scss/_layout.scss @@ -159,4 +159,17 @@ progress{ background-color: $busy; } } -} \ No newline at end of file +} + +.guacamole-display { + cursor: none; + width: 100%; + height: fit-content; + display: flex; + align-items: center; + place-content: center; + background-color: #282828; + & > div { + z-index: 1; + } +} diff --git a/src/sunstone/public/scss/_table.scss b/src/sunstone/public/scss/_table.scss index 719f4da664..9a310b47e3 100644 --- a/src/sunstone/public/scss/_table.scss +++ b/src/sunstone/public/scss/_table.scss @@ -136,14 +136,24 @@ button.search-dropdown { border-style: none !important; } -button.remote_vm { +button.remote-vm { margin: 0 5px; - font-size: 1.2em; + font-size: 1.4em; &.spice i { -webkit-text-stroke: 0.7px; } + &.ssh { + border: .15em solid $anchor-color; + padding: .16em .2em; + font-size: .8em; + + &:hover { + border-color: $anchor-color-hover; + } + } + i { color: $anchor-color; } @@ -159,3 +169,15 @@ button.remote_vm { outline: 0; } } + +a.rdp, +a.save-rdp { + display: flex !important; + align-items: baseline; + + &:hover { + background-color: rgba($color: $anchor-color-hover, $alpha: .6); + color: #ffffff; + font-weight: 600; + } +} diff --git a/src/sunstone/sunstone-server.rb b/src/sunstone/sunstone-server.rb index 9caf23556c..b093741bd3 100755 --- a/src/sunstone/sunstone-server.rb +++ b/src/sunstone/sunstone-server.rb @@ -251,9 +251,11 @@ if $conf[:webauthn_avail] end #start VNC proxy - $vnc = OpenNebulaVNC.new($conf, logger) +#init Guacamole server +$guac = OpenNebulaGuac.new($conf, logger) + configure do set :run, false set :vnc, $vnc @@ -1035,6 +1037,16 @@ post '/vm/:id/startvnc' do @SunstoneServer.startvnc(vm_id, $vnc) end +############################################################################## +# Start Guacamole Session for a target VM +############################################################################## +post '/vm/:id/guac/:type' do + vm_id = params[:id] + type_connection = params[:type] + + @SunstoneServer.startguac(vm_id, type_connection, $guac) +end + ############################################################################## # Perform an action on a Resource ############################################################################## diff --git a/src/sunstone/views/index.erb b/src/sunstone/views/index.erb index 414ae55759..b001e1b110 100644 --- a/src/sunstone/views/index.erb +++ b/src/sunstone/views/index.erb @@ -41,6 +41,7 @@ 'threshold_min' : '<%= $conf[:threshold_min] %>', 'threshold_low' : '<%= $conf[:threshold_low] %>', 'threshold_high' : '<%= $conf[:threshold_high] %>', + 'guac_port' : '<%= $conf[:guac_port] %>', }, 'system_config' : { 'paginate': '<%= $conf[:paginate] %>',