1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-01-18 06:03:39 +03:00

F OpenNebula/one#6599: External VNC management (#3304)

Co-authored-by: Tino Vázquez <cvazquez@opennebula.io>
This commit is contained in:
Jorge Miguel Lobo Escalona 2024-11-21 18:13:03 +01:00 committed by GitHub
parent fa73142e7d
commit 13b6f4aaa4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 141 additions and 26 deletions

View File

@ -55,7 +55,7 @@ const showSupportTab = (routes = [], find = true) => {
const SunstoneApp = () => { const SunstoneApp = () => {
const [getSupport, { isSuccess }] = useLazyCheckOfficialSupportQuery() const [getSupport, { isSuccess }] = useLazyCheckOfficialSupportQuery()
const { changeAppTitle } = useGeneralApi() const { changeAppTitle } = useGeneralApi()
const { isLogged } = useAuth() const { isLogged, externalRedirect } = useAuth()
const { views, view } = useViews() const { views, view } = useViews()
useEffect(() => { useEffect(() => {
@ -95,7 +95,10 @@ const SunstoneApp = () => {
<NotifierUpload /> <NotifierUpload />
</> </>
)} )}
<Router redirectWhenAuth={PATH.DASHBOARD} endpoints={endpoints} /> <Router
redirectWhenAuth={externalRedirect || PATH.DASHBOARD}
endpoints={endpoints}
/>
</AuthLayout> </AuthLayout>
) )
} }

View File

@ -40,7 +40,7 @@ import { Translate } from 'client/components/HOC'
import { GuacamoleSession, T } from 'client/constants' import { GuacamoleSession, T } from 'client/constants'
import { downloadFile } from 'client/utils' import { downloadFile } from 'client/utils'
import { useLazyGetGuacamoleSessionQuery } from 'client/features/OneApi/vm' import { useLazyGetGuacamoleSessionFileQuery } from 'client/features/OneApi/vm'
const useStyles = makeStyles(({ palette }) => ({ const useStyles = makeStyles(({ palette }) => ({
customPopper: { customPopper: {
@ -376,22 +376,20 @@ const GuacamoleDownloadConButton = memo(
* @returns {ReactElement} Button to make screenshot form current session * @returns {ReactElement} Button to make screenshot form current session
*/ */
(session) => { (session) => {
const [getSession] = useLazyGetGuacamoleSessionQuery() const [getSession] = useLazyGetGuacamoleSessionFileQuery()
const { id, vmID, client, typeConnection } = session
const { id, vmID, client } = session
const handleClick = useCallback(async () => { const handleClick = useCallback(async () => {
if (!client) return if (!client) return
const res = await getSession({ const res = await getSession({
id: vmID, id: vmID,
type: 'rdp', type: typeConnection,
download: true, download: true,
}).unwrap() }).unwrap()
if (res) { if (res) {
const blob = new Blob([atob(res)], { type: 'text/plain' }) const blob = new Blob([atob(res)], { type: 'text/plain' })
downloadFile(new File([blob], 'connection.txt')) downloadFile(new File([blob], `${id}.${typeConnection}`))
} }
}, [client]) }, [client])
@ -402,6 +400,14 @@ const GuacamoleDownloadConButton = memo(
title={ title={
<Typography variant="subtitle2"> <Typography variant="subtitle2">
<Translate word={T.DownloadConecctionFile} /> <Translate word={T.DownloadConecctionFile} />
<br />
<Translate
word={
typeConnection === 'rdp'
? T.DownloadConnectionRDP
: T.DownloadConnectionVNC
}
/>
</Typography> </Typography>
} }
> >

View File

@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and * * See the License for the specific language governing permissions and *
* limitations under the License. * * limitations under the License. *
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
import { Redirect, Route } from 'react-router-dom' import { Redirect, Route, useLocation } from 'react-router-dom'
import { useAuth } from 'client/features/Auth' import { useAuth, useAuthApi } from 'client/features/Auth'
import { useEffect } from 'react'
/** /**
* Private route. * Private route.
@ -26,6 +27,12 @@ import { useAuth } from 'client/features/Auth'
*/ */
const ProtectedRoute = (props) => { const ProtectedRoute = (props) => {
const { isLogged: isAuthenticated } = useAuth() const { isLogged: isAuthenticated } = useAuth()
const { changeExternalRedirect } = useAuthApi()
const { pathname, search } = useLocation()
useEffect(() => {
!isAuthenticated && changeExternalRedirect(`${pathname}${search}`)
}, [])
return isAuthenticated ? <Route {...props} /> : <Redirect to="/" /> return isAuthenticated ? <Route {...props} /> : <Redirect to="/" />
} }

View File

@ -1875,7 +1875,9 @@ module.exports = {
ImportIntoDatastore: 'Import into Datastore', ImportIntoDatastore: 'Import into Datastore',
DownloadAppToOpenNebula: 'Download App to OpenNebula', DownloadAppToOpenNebula: 'Download App to OpenNebula',
DownloadApp: 'Download App', DownloadApp: 'Download App',
DownloadConecctionFile: 'Download connection File', DownloadConecctionFile: 'Download connection file.',
DownloadConnectionRDP: 'This file is for the Microsoft remote desktop app',
DownloadConnectionVNC: 'This file is for the TigerVNC Viewer app',
DownloadDefaultImage: 'Download Default Image', DownloadDefaultImage: 'Download Default Image',
ExportAppNameConcept: ExportAppNameConcept:
'Name that the resource will get for description purposes', 'Name that the resource will get for description purposes',

View File

@ -184,8 +184,11 @@ const Guacamole = () => {
<GuacamoleReconnectReadOnlyButton {...session} /> <GuacamoleReconnectReadOnlyButton {...session} />
)} )}
{type === VM_ACTIONS.SSH && <GuacamoleSSHParams {...session} />} {type === VM_ACTIONS.SSH && <GuacamoleSSHParams {...session} />}
{type === VM_ACTIONS.RDP && ( {[VM_ACTIONS.VNC, VM_ACTIONS.RDP].includes(type) && (
<GuacamoleDownloadConButton {...session} /> <GuacamoleDownloadConButton
{...session}
typeConnection={type}
/>
)} )}
<GuacamoleReconnectButton {...session} /> <GuacamoleReconnectButton {...session} />
<GuacamoleScreenshotButton {...session} /> <GuacamoleScreenshotButton {...session} />

View File

@ -15,21 +15,21 @@
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */ /* eslint-disable jsdoc/require-jsdoc */
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { useDispatch, useSelector, shallowEqual } from 'react-redux' import { shallowEqual, useDispatch, useSelector } from 'react-redux'
import { ResourceView } from 'client/apps/sunstone/routes'
import {
DEFAULT_LANGUAGE,
DEFAULT_SCHEME,
ONEADMIN_ID,
RESOURCE_NAMES,
_APPS,
} from 'client/constants'
import { actions, name as authSlice, logout } from 'client/features/Auth/slice'
import { name as generalSlice } from 'client/features/General/slice' import { name as generalSlice } from 'client/features/General/slice'
import { name as authSlice, actions, logout } from 'client/features/Auth/slice'
import groupApi from 'client/features/OneApi/group' import groupApi from 'client/features/OneApi/group'
import systemApi from 'client/features/OneApi/system' import systemApi from 'client/features/OneApi/system'
import { ResourceView } from 'client/apps/sunstone/routes'
import { areStringEqual } from 'client/models/Helper' import { areStringEqual } from 'client/models/Helper'
import {
_APPS,
RESOURCE_NAMES,
ONEADMIN_ID,
DEFAULT_SCHEME,
DEFAULT_LANGUAGE,
} from 'client/constants'
const APPS_WITH_VIEWS = [_APPS.sunstone].map((app) => app.toLowerCase()) const APPS_WITH_VIEWS = [_APPS.sunstone].map((app) => app.toLowerCase())
@ -126,6 +126,8 @@ export const useAuthApi = () => {
changeJwt: (jwt) => dispatch(actions.changeJwt(jwt)), changeJwt: (jwt) => dispatch(actions.changeJwt(jwt)),
changeAuthUser: (user) => dispatch(actions.changeAuthUser(user)), changeAuthUser: (user) => dispatch(actions.changeAuthUser(user)),
setErrorMessage: (message) => dispatch(actions.setErrorMessage(message)), setErrorMessage: (message) => dispatch(actions.setErrorMessage(message)),
changeExternalRedirect: (url) =>
dispatch(actions.changeExternalRedirect(url)),
} }
} }

View File

@ -25,6 +25,7 @@ const initial = () => ({
user: null, user: null,
filterPool: FILTER_POOL.ALL_RESOURCES, filterPool: FILTER_POOL.ALL_RESOURCES,
isLoginInProgress: false, isLoginInProgress: false,
externalRedirect: '',
}) })
const slice = createSlice({ const slice = createSlice({
@ -38,6 +39,9 @@ const slice = createSlice({
state.isLoginInProgress = isLoginInProgress state.isLoginInProgress = isLoginInProgress
} }
}, },
changeExternalRedirect: (state, { payload }) => {
state.externalRedirect = payload
},
changeJwt: (state, { payload }) => { changeJwt: (state, { payload }) => {
state.jwt = payload state.jwt = payload
}, },

View File

@ -144,6 +144,23 @@ const vmApi = oneApi.injectEndpoints({
resource: 'VM', resource: 'VM',
}), }),
}), }),
getGuacamoleSessionFile: builder.query({
/**
* Returns a Guacamole session data.
*
* @param {object} params - Request parameters
* @param {string} params.id - Virtual machine id
* @param {'vnc'|'ssh'|'rdp'} params.type - Connection type
* @returns {string} The session token
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = ExtraActions.GUACAMOLE
const command = { name, ...ExtraCommands[name] }
return { params, command }
},
}),
getGuacamoleSession: builder.query({ getGuacamoleSession: builder.query({
/** /**
* Returns a Guacamole session. * Returns a Guacamole session.
@ -1277,6 +1294,8 @@ export const {
useLazyGetVmQuery, useLazyGetVmQuery,
useGetGuacamoleSessionQuery, useGetGuacamoleSessionQuery,
useLazyGetGuacamoleSessionQuery, useLazyGetGuacamoleSessionQuery,
useGetGuacamoleSessionFileQuery,
useLazyGetGuacamoleSessionFileQuery,
useGetMonitoringQuery, useGetMonitoringQuery,
useLazyGetMonitoringQuery, useLazyGetMonitoringQuery,
useGetMonitoringPoolQuery, useGetMonitoringPoolQuery,

View File

@ -35,6 +35,8 @@ const {
defaultCommandVM, defaultCommandVM,
defaultTypeCrypto, defaultTypeCrypto,
defaultHash, defaultHash,
keysRDP,
keysVNC,
} = defaults } = defaults
const appConfig = getSunstoneConfig() const appConfig = getSunstoneConfig()
@ -163,9 +165,29 @@ const generateGuacamoleSession = (
...connection.connection.settings, ...connection.connection.settings,
protocol: ensuredType, protocol: ensuredType,
} }
const encodedData = btoa( const encodedData = btoa(
Object.entries(contentFile) Object.entries(contentFile)
.map(([key, value]) => `${key}=${value}`) .map(([key, value]) => {
let rtn
const keys = type === 'rdp' ? keysRDP : keysVNC
// eslint-disable-next-line no-prototype-builtins
if (keys.hasOwnProperty(key)) {
const getValue =
value !== null && typeof value !== 'undefined'
? value
: keys[key].value
const parseValue =
typeof getValue === 'boolean'
? `${+(keys[key].reverse ? !getValue : getValue)}`
: `${getValue}`
rtn = `${keys[key].key}${parseValue}`
}
return rtn
})
.filter(Boolean)
.join('\n') .join('\n')
) )
@ -318,7 +340,7 @@ const getRdpSettings = (vmInfo) => {
config['disable-glyph-caching'] = config['disable-glyph-caching'] =
nicWithRdp?.RDP_DISABLE_GLYPH_CACHING?.toLowerCase() === 'yes' nicWithRdp?.RDP_DISABLE_GLYPH_CACHING?.toLowerCase() === 'yes'
if (config.username && config.password) config.security = 'nla' if (config.username && config.password) config.security = 'rdp'
return config return config
} }

View File

@ -188,6 +188,53 @@ const defaults = {
hookObjectNames: { hookObjectNames: {
vn: 'net', vn: 'net',
}, },
keysRDP: {
hostname: { key: 'full address:s:', value: '' },
username: { key: 'username:s:', value: '' },
password: { key: 'password 51:b:', value: '' },
port: { key: 'server port:i:', value: '' },
'server-layout': { key: 'keyboard layout:i:', value: '' },
'disable-audio': { key: 'audiomode:i:', value: 0 },
'enable-audio-input': { key: 'redirectaudiocapture:1:', value: 0 },
'enable-wallpaper': {
key: 'disable wallpaper:i:',
value: 0,
reverse: true,
},
'enable-theming': { key: 'disable themes:i:', value: 0, reverse: true },
'enable-font-smoothing': { key: 'allow font smoothing:i:', value: 1 },
'enable-full-window-drag': {
key: 'disable full window drag:i:',
value: 0,
reverse: true,
},
'enable-desktop-composition': {
key: 'allow desktop composition:i:',
value: 1,
},
'enable-menu-animations': {
key: 'disable menu anims:i:',
value: 0,
reverse: true,
},
'disable-bitmap-caching': {
key: 'bitmapcachepersistenable:i:',
value: 0,
reverse: true,
},
'disable-offscreen-caching': {
key: 'offscreen caching:i:',
value: 1,
reverse: true,
},
'disable-glyph-caching': { key: 'glyphcache:i:', value: 0, reverse: true },
},
keysVNC: {
hostname: { key: 'Host=', value: '' },
port: { key: 'Port=', value: '' },
username: { key: 'Username=', value: '' },
password: { key: 'Password=', value: '' },
},
} }
module.exports = defaults module.exports = defaults