From 952cecb74d8091f1e479a73edd9adfadcf730f3e Mon Sep 17 00:00:00 2001 From: Jorge Miguel Lobo Escalona Date: Thu, 17 Mar 2022 13:42:09 +0100 Subject: [PATCH] F #5749: Creation ticket VMRC on FireEdge (#1848) Co-authored-by: Frederick Ernesto Borges Noronha (cherry picked from commit 9ed9b670bd27526526757ef73ca1959e8971cd44) --- .../routes/api/oneflow/template/functions.js | 14 +- .../server/routes/api/vcenter/functions.js | 199 +++++++++++++++++- .../src/server/routes/api/vcenter/index.js | 6 + .../src/server/routes/api/vcenter/routes.js | 12 ++ .../src/server/routes/websockets/vmrc.js | 13 +- src/sunstone/models/SunstoneServer.rb | 28 --- src/sunstone/models/sunstone_vmrc.rb | 101 --------- src/sunstone/public/app/opennebula/vm.js | 8 +- src/sunstone/sunstone-server.rb | 13 -- 9 files changed, 230 insertions(+), 164 deletions(-) delete mode 100644 src/sunstone/models/sunstone_vmrc.rb diff --git a/src/fireedge/src/server/routes/api/oneflow/template/functions.js b/src/fireedge/src/server/routes/api/oneflow/template/functions.js index ec9d6e69d3..5b826e01a0 100644 --- a/src/fireedge/src/server/routes/api/oneflow/template/functions.js +++ b/src/fireedge/src/server/routes/api/oneflow/template/functions.js @@ -17,7 +17,7 @@ const { Validator } = require('jsonschema') const { role, service, action } = require('server/routes/api/oneflow/schemas') const { - oneFlowConection, + oneFlowConnection, returnSchemaError, } = require('server/routes/api/oneflow/utils') const { defaults, httpCodes } = require('server/utils/constants') @@ -91,13 +91,13 @@ const serviceTemplate = ( if (params && params.id) { config.path = '/service_template/{0}' config.request = params.id - oneFlowConection( + oneFlowConnection( config, (data) => success(next, res, data), (data) => error(next, res, data) ) } else { - oneFlowConection( + oneFlowConnection( config, (data) => success(next, res, data), (data) => error(next, res, data) @@ -130,7 +130,7 @@ const serviceTemplateDelete = ( password, request: params.id, } - oneFlowConection( + oneFlowConnection( config, (data) => success(next, res, data), (data) => error(next, res, data) @@ -173,7 +173,7 @@ const serviceTemplateCreate = ( password, post: template, } - oneFlowConection( + oneFlowConnection( config, (data) => success(next, res, data), (data) => error(next, res, data) @@ -224,7 +224,7 @@ const serviceTemplateUpdate = ( request: params.id, post: template, } - oneFlowConection( + oneFlowConnection( config, (data) => success(next, res, data), (data) => error(next, res, data) @@ -275,7 +275,7 @@ const serviceTemplateAction = ( request: params.id, post: template, } - oneFlowConection( + oneFlowConnection( config, (data) => success(next, res, data), (data) => error(next, res, data) diff --git a/src/fireedge/src/server/routes/api/vcenter/functions.js b/src/fireedge/src/server/routes/api/vcenter/functions.js index 4e3f461e23..b7081eaad9 100644 --- a/src/fireedge/src/server/routes/api/vcenter/functions.js +++ b/src/fireedge/src/server/routes/api/vcenter/functions.js @@ -13,32 +13,43 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ +const btoa = require('btoa') +const https = require('https') +// eslint-disable-next-line node/no-deprecated-api +const { parse } = require('url') + +const { request: axios } = require('axios') + const { defaults, httpCodes } = require('server/utils/constants') const { httpResponse, executeCommand, executeCommandAsync, publish, + getSunstoneAuth, } = require('server/utils/server') const { consoleParseToString, consoleParseToJSON, } = require('server/utils/opennebula') +const { createTokenServerAdmin } = require('server/routes/api/auth/utils') +const { Actions: ActionHost } = require('server/utils/constants/commands/host') +const { Actions: ActionVM } = require('server/utils/constants/commands/vm') const { resourceFromData, resources, params: commandParams, } = require('server/routes/api/vcenter/command-flags') const { getSunstoneConfig } = require('server/utils/yml') - const { + httpMethod, defaultEmptyFunction, defaultCommandVcenter, defaultRegexpStartJSON, defaultRegexpEndJSON, defaultRegexpSplitLine, } = defaults - +const { POST } = httpMethod const { ok, internalServerError, badRequest, accepted } = httpCodes const { LIST, IMPORT } = resourceFromData const appConfig = getSunstoneConfig() @@ -50,6 +61,7 @@ const regexExclude = [ /^\u001b\[.*?m\u001b\[.*?m# vCenter.*/i, ] const regexHeader = /^IMID,.*/i +const regexGetVcenterId = /-(?.*)_/s const validObjects = Object.values(resources) @@ -373,11 +385,194 @@ const importVobject = ( httpReturn(accepted) } +/** + * Axios request. + * + * @param {object} params - Axios params + * @param {Function} callback - Success Axios callback + * @param {Function} error - Error Axios callback + */ +const request = ( + params = {}, + callback = defaultEmptyFunction, + error = defaultEmptyFunction +) => { + const defaultsProperties = { + method: POST, + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + }), + validateStatus: (status) => status, + } + axios({ + ...defaultsProperties, + ...params, + }) + .then((response) => { + if (response && response.statusText) { + if (response.status >= 200 && response.status < 400) { + if (response.data) { + return response.data + } + } + throw Error(response.data) + } else if (response.data) { + throw Error(response.data) + } + }) + .then((data) => { + callback(data) + }) + .catch((e) => { + error(e) + }) +} + +/** + * Get system config. + * + * @param {object} res - http response + * @param {Function} next - express stepper + * @param {object} params - params of http request + * @param {object} userData - user of http request + * @param {function(string, string): Function} oneConnection - One Connection + */ +const getToken = ( + res = {}, + next = defaultEmptyFunction, + params = {}, + userData = {}, + oneConnection = defaultEmptyFunction +) => { + const { username, key, iv } = getSunstoneAuth() + const { id } = params + const responser = (code = badRequest, data = '') => { + res.locals.httpCode = httpResponse(code, data, '') + next() + } + + if (!(username && key && iv) || !Number.isInteger(parseInt(id, 10))) { + responser() + + return + } + + const tokenWithServerAdmin = createTokenServerAdmin({ + serverAdmin: username, + username, + key, + iv, + }) + if (!tokenWithServerAdmin.token) { + responser() + + return + } + + const connect = oneConnection( + `${username}:${username}`, + tokenWithServerAdmin.token + ) + + connect(ActionVM.VM_INFO, [parseInt(id, 10), true], (err, vminfo) => { + if ( + !( + vminfo && + vminfo.VM && + vminfo.VM.DEPLOY_ID && + vminfo.VM.HISTORY_RECORDS && + vminfo.VM.HISTORY_RECORDS.HISTORY + ) || + err + ) { + responser(internalServerError) + } + + const history = vminfo.VM.HISTORY_RECORDS.HISTORY + const arrayHistory = Array.isArray(history) ? history : [history] + + const hostID = parseInt( + arrayHistory.reduce( + (max, record) => (record.SEQ > max.SEQ ? record : max), + arrayHistory[0] + ).HID, + 10 + ) + + const vmid = vminfo.VM.DEPLOY_ID.match(regexGetVcenterId).groups.id + + connect(ActionHost.HOST_INFO, [hostID, true], (err, hostinfo) => { + if ( + !( + hostinfo && + hostinfo.HOST && + hostinfo.HOST.TEMPLATE && + hostinfo.HOST.TEMPLATE.VCENTER_HOST && + hostinfo.HOST.TEMPLATE.VCENTER_USER && + hostinfo.HOST.TEMPLATE.VCENTER_PASSWORD + ) || + err + ) { + responser(internalServerError) + + return + } + + const { VCENTER_HOST, VCENTER_USER, VCENTER_PASSWORD } = + hostinfo.HOST.TEMPLATE + + const responseInternalServer = () => { + responser(internalServerError) + } + + const genToken = (data) => { + request( + { + url: `https://${VCENTER_HOST}/api/vcenter/vm/vm-${vmid}/console/tickets`, + headers: { + 'Content-Type': 'application/json', + 'vmware-api-session-id': data, + }, + data: JSON.stringify({ type: 'WEBMKS' }), + }, + (success) => { + const { ticket } = success + const { protocol, hostname, port, path } = parse(ticket) + + const httpProtocol = protocol === 'wss:' ? 'https' : 'http' + const esxUrl = `${httpProtocol}://${hostname}:${port}` + const token = path.replace('/ticket/', '') + global.vcenterToken = { [token]: esxUrl } + responser(ok, { + ticket: token, + }) + }, + responseInternalServer + ) + } + + request( + { + url: `https://${VCENTER_HOST}/api/session`, + headers: { + Authorization: `Basic ${btoa( + `${VCENTER_USER}:${VCENTER_PASSWORD}` + )}`, + }, + }, + genToken, + responseInternalServer + ) + }) + }) +} + const functionRoutes = { list, listAll, cleartags, importHost, importVobject, + getToken, } module.exports = functionRoutes diff --git a/src/fireedge/src/server/routes/api/vcenter/index.js b/src/fireedge/src/server/routes/api/vcenter/index.js index 9dfa5f4034..b9d9c4e5bd 100644 --- a/src/fireedge/src/server/routes/api/vcenter/index.js +++ b/src/fireedge/src/server/routes/api/vcenter/index.js @@ -21,12 +21,14 @@ const { listAll, cleartags, importHost, + getToken, } = require('server/routes/api/vcenter/functions') const { resources } = require('server/routes/api/vcenter/command-flags') const { TEMPLATES, DATASTORES, NETWORKS, IMAGES } = resources const { + VCENTER_TOKEN, VCENTER_CLEAR_TAGS, VCENTER_IMPORT_HOSTS, VCENTER_IMPORT_DATASTORES, @@ -38,6 +40,10 @@ const { } = Actions module.exports = [ + { + ...Commands[VCENTER_TOKEN], + action: getToken, + }, { ...Commands[VCENTER_CLEAR_TAGS], action: cleartags, diff --git a/src/fireedge/src/server/routes/api/vcenter/routes.js b/src/fireedge/src/server/routes/api/vcenter/routes.js index 0db36fc2ab..99bcc968c3 100644 --- a/src/fireedge/src/server/routes/api/vcenter/routes.js +++ b/src/fireedge/src/server/routes/api/vcenter/routes.js @@ -23,6 +23,7 @@ const basepath = '/vcenter' const { POST, GET } = httpMethod const { resource, postBody, query } = fromData +const VCENTER_TOKEN = 'vcenter.token' const VCENTER_CLEAR_TAGS = 'vcenter.cleartags' const VCENTER_IMPORT_HOSTS = 'vcenter.importhosts' const VCENTER_IMPORT_DATASTORES = 'vcenter.importdatastores' @@ -32,6 +33,7 @@ const VCENTER_IMPORT_IMAGES = 'vcenter.importimages' const VCENTER_LIST_ALL = 'vcenter.listall' const VCENTER_LIST = 'vcenter.list' const Actions = { + VCENTER_TOKEN, VCENTER_CLEAR_TAGS, VCENTER_IMPORT_HOSTS, VCENTER_IMPORT_TEMPLATES, @@ -45,6 +47,16 @@ const Actions = { module.exports = { Actions, Commands: { + [VCENTER_TOKEN]: { + path: `${basepath}/token/:id`, + httpMethod: GET, + auth: true, + params: { + id: { + from: resource, + }, + }, + }, [VCENTER_CLEAR_TAGS]: { path: `${basepath}/cleartags/:id`, httpMethod: POST, diff --git a/src/fireedge/src/server/routes/websockets/vmrc.js b/src/fireedge/src/server/routes/websockets/vmrc.js index 4bc8bd043e..4c70274dea 100644 --- a/src/fireedge/src/server/routes/websockets/vmrc.js +++ b/src/fireedge/src/server/routes/websockets/vmrc.js @@ -17,7 +17,6 @@ // eslint-disable-next-line node/no-deprecated-api const { parse } = require('url') const { createProxyMiddleware } = require('http-proxy-middleware') -const { readFileSync } = require('fs-extra') const { getFireedgeConfig } = require('server/utils/yml') const { messageTerminal } = require('server/utils/general') const { @@ -55,14 +54,10 @@ const vmrcProxy = createProxyMiddleware(endpointVmrc, { if (parseURL && parseURL.pathname) { const ticket = parseURL.pathname.split('/')[3] writeInLogger(ticket, 'path to vmrc token: %s') - try { - const esxi = readFileSync( - `${global.paths.VMRC_TOKENS || ''}/${ticket}` - ).toString() - - return esxi - } catch (error) { - writeInLogger(ticket, 'Error to read vmrc token file: %s') + if (global && global.vcenterToken && global.vcenterToken[ticket]) { + return global.vcenterToken[ticket] + } else { + writeInLogger(ticket, 'Non-existent token: %s') } } } diff --git a/src/sunstone/models/SunstoneServer.rb b/src/sunstone/models/SunstoneServer.rb index 04d0a249c2..150265d8f7 100644 --- a/src/sunstone/models/SunstoneServer.rb +++ b/src/sunstone/models/SunstoneServer.rb @@ -21,7 +21,6 @@ include OpenNebulaJSON require 'sunstone_vnc' require 'sunstone_guac' -require 'sunstone_vmrc' require 'sunstone_vm_helper' require 'OpenNebulaAddons' require 'OpenNebulaJSON/JSONUtils' @@ -337,33 +336,6 @@ class SunstoneServer < CloudServer return guac.proxy(resource, type_connection, client) end - ######################################################################## - # VMRC - ######################################################################## - def startvmrc(id, vmrc, _client=nil) - resource = retrieve_resource("vm", id) - if OpenNebula.is_error?(resource) - return [404, resource.to_json] - end - 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 - - client = _client.nil? ? @client : _client - return vmrc.proxy(resource, client) - end - ######################################################################## # Accounting & Monitoring ######################################################################## diff --git a/src/sunstone/models/sunstone_vmrc.rb b/src/sunstone/models/sunstone_vmrc.rb deleted file mode 100644 index 928632821b..0000000000 --- a/src/sunstone/models/sunstone_vmrc.rb +++ /dev/null @@ -1,101 +0,0 @@ -# -------------------------------------------------------------------------- # -# Copyright 2002-2021, 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 vmrc proxy server # -#----------------------------------------------------------------------------# - -require 'rubygems' -require 'json' -require 'opennebula' -require 'base64' -require 'openssl' -require 'vcenter_driver' -require 'fileutils' -require 'sunstone_remotes' - -if !ONE_LOCATION - VMRC_TICKETS = '/var/lib/one/sunstone_vmrc_tokens/' -else - VMRC_TICKETS = ONE_LOCATION + '/var/sunstone_vmrc_tokens/' -end - -FileUtils.mkdir_p VMRC_TICKETS - -# Class for necessary VMRC ticket creation -class SunstoneVMRC < SunstoneRemoteConnections - - attr_reader :proxy_port - - def initialize(logger, options = {}) - super - end - - def proxy(vm_resource, client = nil) - # Check configurations and VM attributes - unless allowed_console_states.include?(vm_resource['LCM_STATE']) - error_message = "Wrong state (#{vm_resource['LCM_STATE']}) to - open a VMRC session" - return error(400, error_message) - end - - unless vm_resource['USER_TEMPLATE/HYPERVISOR'] == 'vcenter' - return error(400, 'VMRC Connection is only for vcenter hipervisor') - end - - unless vm_resource['MONITORING/VCENTER_ESX_HOST'] - error_message = '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' - return error(400, error_message) - end - - vm_id = vm_resource['ID'] - one_vm = VCenterDriver::VIHelper.one_item( - OpenNebula::VirtualMachine, - vm_id - ) - vm_ref = one_vm['DEPLOY_ID'] - - host_id = one_vm['HISTORY_RECORDS/HISTORY[last()]/HID'].to_i - - vi_client = VCenterDriver::VIClient.new_from_host(host_id, client) - - vm = VCenterDriver::VirtualMachine.new(vi_client, vm_ref, vm_id) - - parameters = vm.html_console_parameters - - data = { - :host => parameters[:host], - :port => parameters[:port], - :ticket => parameters[:ticket] - } - - file = File.open( - VMRC_TICKETS + - VCenterDriver::FileHelper.sanitize(data[:ticket]), - 'w' - ) - file.write('https://' + data[:host] + ':' + data[:port].to_s) - file.close - - info = SunstoneVMHelper.get_remote_info(vm_resource) - encode_info = Base64.encode64(info.to_json) - - [200, { :data => data, :info => encode_info }.to_json] - end - -end diff --git a/src/sunstone/public/app/opennebula/vm.js b/src/sunstone/public/app/opennebula/vm.js index c6af49096a..b4a5d026d6 100644 --- a/src/sunstone/public/app/opennebula/vm.js +++ b/src/sunstone/public/app/opennebula/vm.js @@ -629,14 +629,14 @@ define(function(require) { "vmrc" : function(params) { var callback = params.success; var callback_error = params.error; - var id = params.data.id; + var vm_id = params.data.id; var resource = RESOURCE; var request = OpenNebulaHelper.request(resource, null, params.data); $.ajax({ - url: "vm/" + id + "/startvmrc", - type: "POST", - dataType: "json", + url: Config.publicFireedgeEndpoint + "/fireedge/api/vcenter/token/" + vm_id, + type: "GET", + headers: {"Authorization": fireedge_token}, success: function(response) { return callback ? callback(request, response) : null; }, diff --git a/src/sunstone/sunstone-server.rb b/src/sunstone/sunstone-server.rb index de773ea1bc..dddd351118 100755 --- a/src/sunstone/sunstone-server.rb +++ b/src/sunstone/sunstone-server.rb @@ -281,13 +281,9 @@ $vnc = SunstoneVNC.new($conf, logger) #init Guacamole proxy $guac = SunstoneGuac.new(logger) -#init VMRC proxy -$vmrc = SunstoneVMRC.new(logger) - configure do set :run, false set :vnc, $vnc - set :vmrc, $vmrc set :erb, :trim => '-' end @@ -1210,15 +1206,6 @@ post '/vm/:id/guac/:type' do @SunstoneServer.startguac(vm_id, type_connection, $guac, user) end -############################################################################## -# Start VMRC Session for a target VM -############################################################################## -post '/vm/:id/startvmrc' do - vm_id = params[:id] - serveradmin_client = $cloud_auth.client(nil, session[:active_zone_endpoint]) - @SunstoneServer.startvmrc(vm_id, $vmrc, serveradmin_client) -end - ############################################################################## # Perform an action on a Resource ##############################################################################