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:
parent
fa73142e7d
commit
13b6f4aaa4
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -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="/" />
|
||||||
}
|
}
|
||||||
|
@ -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',
|
||||||
|
@ -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} />
|
||||||
|
@ -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)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user