mirror of
https://github.com/OpenNebula/one.git
synced 2025-01-03 01:17:41 +03:00
F OpenNebula/one#6742: Implement loading of OS profiles (#3267)
+ define windows profile OpenNebula/one#6627 * Add windows profile * Adds OS Profiles parsing & loading - new /profiles endpoint - profiles are stored in /etc/fireedge/sunstone/profiles/ - YAML format only * Update profile loading * Load profile only once per step * Add indicator for last applied profile * Fix autocomplete controller equality comparison * Install new 'profiles' directory * Installs windows_optimized profile Signed-off-by: Victor Hansson <vhansson@opennebula.io>
This commit is contained in:
parent
60bfce4146
commit
48f508ea69
@ -280,6 +280,7 @@ ETC_DIRS="$ETC_LOCATION/vmm_exec \
|
||||
$ETC_LOCATION/fireedge/provision/providers.d \
|
||||
$ETC_LOCATION/fireedge/provision/providers.d-extra \
|
||||
$ETC_LOCATION/fireedge/sunstone \
|
||||
$ETC_LOCATION/fireedge/sunstone/profiles \
|
||||
$ETC_LOCATION/fireedge/sunstone/admin \
|
||||
$ETC_LOCATION/fireedge/sunstone/user \
|
||||
$ETC_LOCATION/fireedge/sunstone/groupadmin \
|
||||
@ -888,6 +889,7 @@ INSTALL_FIREEDGE_ETC_FILES=(
|
||||
FIREEDGE_PROVISION_ETC_PROVIDERS:$ETC_LOCATION/fireedge/provision/providers.d
|
||||
FIREEDGE_PROVISION_ETC_PROVIDERS_EXTRA:$ETC_LOCATION/fireedge/provision/providers.d-extra
|
||||
FIREEDGE_SUNSTONE_ETC:$ETC_LOCATION/fireedge/sunstone
|
||||
FIREEDGE_SUNSTONE_ETC_PROFILES:$ETC_LOCATION/fireedge/sunstone/profiles
|
||||
FIREEDGE_SUNSTONE_ETC_VIEW_ADMIN:$ETC_LOCATION/fireedge/sunstone/admin
|
||||
FIREEDGE_SUNSTONE_ETC_VIEW_USER:$ETC_LOCATION/fireedge/sunstone/user
|
||||
FIREEDGE_SUNSTONE_ETC_VIEW_CLOUD:$ETC_LOCATION/fireedge/sunstone/cloud
|
||||
@ -2777,6 +2779,8 @@ FIREEDGE_PROVISION_ETC_PROVIDERS_EXTRA="src/fireedge/etc/provision/providers.d-e
|
||||
FIREEDGE_SUNSTONE_ETC="src/fireedge/etc/sunstone/sunstone-server.conf \
|
||||
src/fireedge/etc/sunstone/sunstone-views.yaml"
|
||||
|
||||
FIREEDGE_SUNSTONE_ETC_PROFILES="src/fireedge/etc/sunstone/profiles/windows_optimized.yaml"
|
||||
|
||||
FIREEDGE_SUNSTONE_ETC_VIEW_ADMIN="src/fireedge/etc/sunstone/admin/vm-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/vm-template-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/vm-group-tab.yaml \
|
||||
|
53
src/fireedge/etc/sunstone/profiles/windows_optimized.yaml
Normal file
53
src/fireedge/etc/sunstone/profiles/windows_optimized.yaml
Normal file
@ -0,0 +1,53 @@
|
||||
---
|
||||
# This profile defines some default windows optimizations
|
||||
"General":
|
||||
NAME: "Optimized Windows Profile"
|
||||
|
||||
"Advanced options":
|
||||
OsCpu:
|
||||
OS:
|
||||
ARCH: X86_64
|
||||
SD_DISK_BUS: scsi
|
||||
MACHINE: host-passthrough
|
||||
FIRMWARE: BIOS
|
||||
FEATURES:
|
||||
ACPI: "Yes"
|
||||
PAE: "Yes"
|
||||
APIC: "Yes"
|
||||
HYPERV: "Yes"
|
||||
LOCALTIME: "Yes"
|
||||
GUEST_AGENT: "Yes"
|
||||
VIRTIO_SCSI_QUEUES: "auto"
|
||||
VIRTIO_BLK_QUEUES: "auto"
|
||||
# IOTHREADS:
|
||||
CPU_MODEL:
|
||||
MODEL: "host-passthrough"
|
||||
# FEATURES:
|
||||
# - Tunable depending on host CPU support
|
||||
# -
|
||||
RAW:
|
||||
DATA: |-
|
||||
<features>
|
||||
<hyperv>
|
||||
<evmcs state='off'/>
|
||||
<frequencies state='on'/>
|
||||
<ipi state='on'/>
|
||||
<reenlightenment state='off'/>
|
||||
<relaxed state='on'/>
|
||||
<reset state='off'/>
|
||||
<runtime state='on'/>
|
||||
<spinlocks state='on' retries='8191'/>
|
||||
<stimer state='on'/>
|
||||
<synic state='on'/>
|
||||
<tlbflush state='on'/>
|
||||
<vapic state='on'/>
|
||||
<vpindex state='on'/>
|
||||
</hyperv>
|
||||
</features>
|
||||
<clock offset='utc'>
|
||||
<timer name='hpet' present='no'/>
|
||||
<timer name='hypervclock' present='yes'/>
|
||||
<timer name='pit' tickpolicy='delay'/>
|
||||
<timer name='rtc' tickpolicy='catchup'/>
|
||||
</clock>
|
||||
VALIDATE: "Yes"
|
@ -149,7 +149,11 @@ const AutocompleteController = memo(
|
||||
typeof option === 'object' ? option.text : option
|
||||
}
|
||||
isOptionEqualToValue={(option, value) =>
|
||||
typeof option === 'object' ? option.value === value : option === value
|
||||
typeof option === 'object' && typeof value === 'object'
|
||||
? option?.value === value?.value
|
||||
: typeof option === 'object'
|
||||
? option?.value === value || option?.text === value
|
||||
: option === value
|
||||
}
|
||||
renderInput={({ inputProps, ...inputParams }) => (
|
||||
<TextField
|
||||
|
@ -13,21 +13,68 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useCallback } from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useLazyGetOsProfilesQuery } from 'client/features/OneApi/system'
|
||||
import { STEP_MAP, T, TAB_FORM_MAP } from 'client/constants'
|
||||
import {
|
||||
deepmerge,
|
||||
cleanEmpty,
|
||||
cloneObject,
|
||||
set,
|
||||
flattenObjectByKeys,
|
||||
} from 'client/utils'
|
||||
import { object } from 'yup'
|
||||
import { useFormContext, useWatch } from 'react-hook-form'
|
||||
import { Box } from '@mui/material'
|
||||
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { AttributePanel } from 'client/components/Tabs/Common'
|
||||
import { cleanEmpty, cloneObject, set } from 'client/utils'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const STEP_ID = 'custom-variables'
|
||||
|
||||
const Content = () => {
|
||||
const { setValue } = useFormContext()
|
||||
const Content = ({ isUpdate }) => {
|
||||
const { setValue, reset, getValues, watch } = useFormContext()
|
||||
const { useLoadOsProfile } = useGeneralApi()
|
||||
const customVars = useWatch({ name: STEP_ID })
|
||||
|
||||
const [fetchProfile] = useLazyGetOsProfilesQuery()
|
||||
const osProfile = watch('general.OS_PROFILE')
|
||||
const profileIsLoaded =
|
||||
useSelector((state) => state?.general?.loadedOsProfile?.[STEP_ID]) || false
|
||||
|
||||
// Prefill current step based on profile
|
||||
useEffect(async () => {
|
||||
if (!profileIsLoaded && osProfile && osProfile !== '-') {
|
||||
try {
|
||||
const { data: fetchedProfile } = await fetchProfile({ id: osProfile })
|
||||
const currentForm = getValues()
|
||||
const mappedSteps = Object.fromEntries(
|
||||
Object.entries(fetchedProfile).map(([key, value]) => [
|
||||
STEP_MAP[key] || key,
|
||||
value,
|
||||
])
|
||||
)
|
||||
|
||||
const flattenByKeys = Object.keys(TAB_FORM_MAP)
|
||||
mappedSteps.extra = flattenObjectByKeys(
|
||||
mappedSteps.extra,
|
||||
flattenByKeys
|
||||
)
|
||||
|
||||
const mergedSteps = deepmerge(currentForm, mappedSteps)
|
||||
|
||||
reset(mergedSteps, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
keepDefaultValues: true,
|
||||
})
|
||||
|
||||
useLoadOsProfile({ stepId: STEP_ID })
|
||||
} catch (error) {}
|
||||
}
|
||||
}, [osProfile])
|
||||
|
||||
const handleChangeAttribute = useCallback(
|
||||
(path, newValue) => {
|
||||
const newCustomVars = cloneObject(customVars)
|
||||
@ -52,17 +99,27 @@ const Content = () => {
|
||||
)
|
||||
}
|
||||
|
||||
Content.propTypes = {
|
||||
isUpdate: PropTypes.bool,
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom variables about VM Template.
|
||||
*
|
||||
* @param {object} params - Props
|
||||
* @param {object} params.apiTemplateDataExtended - VM Template
|
||||
* @returns {object} Custom configuration step
|
||||
*/
|
||||
const CustomVariables = () => ({
|
||||
id: STEP_ID,
|
||||
label: T.CustomVariables,
|
||||
resolver: object(),
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: Content,
|
||||
})
|
||||
const CustomVariables = ({ apiTemplateDataExtended: vmTemplate }) => {
|
||||
const isUpdate = !!vmTemplate?.NAME
|
||||
|
||||
return {
|
||||
id: STEP_ID,
|
||||
label: T.CustomVariables,
|
||||
resolver: object(),
|
||||
optionsValidate: { abortEarly: false },
|
||||
content: (props) => Content({ ...props, isUpdate }),
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomVariables
|
||||
|
@ -74,7 +74,7 @@ export const FEATURES = {
|
||||
const { data: hosts = [] } = useGetHostsQuery()
|
||||
const kvmFeatures = getKvmCpuFeatures(hosts)
|
||||
|
||||
return arrayToOptions(kvmFeatures)
|
||||
return arrayToOptions(kvmFeatures, { addEmpty: false })
|
||||
},
|
||||
validation: array(string().trim()).default(() => []),
|
||||
}
|
||||
|
@ -14,7 +14,8 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { useMemo, ReactElement } from 'react'
|
||||
import { ReactElement, useMemo, useEffect } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { useFormContext, FieldErrors } from 'react-hook-form'
|
||||
@ -22,6 +23,7 @@ import { useFormContext, FieldErrors } from 'react-hook-form'
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
|
||||
import { deepmerge, flattenObjectByKeys } from 'client/utils'
|
||||
import Tabs from 'client/components/Tabs'
|
||||
import Storage from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/storage'
|
||||
import Networking from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/networking'
|
||||
@ -34,10 +36,18 @@ import InputOutput from 'client/components/Forms/VmTemplate/CreateForm/Steps/Ext
|
||||
import Numa from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/numa'
|
||||
import Backup from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/backup'
|
||||
|
||||
import {
|
||||
STEP_MAP,
|
||||
T,
|
||||
RESOURCE_NAMES,
|
||||
VmTemplate,
|
||||
TAB_FORM_MAP,
|
||||
} from 'client/constants'
|
||||
import { STEP_ID as GENERAL_ID } from 'client/components/Forms/VmTemplate/CreateForm/Steps/General'
|
||||
import { SCHEMA } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema'
|
||||
import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper'
|
||||
import { T, RESOURCE_NAMES, VmTemplate } from 'client/constants'
|
||||
import { useLazyGetOsProfilesQuery } from 'client/features/OneApi/system'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
const VROUTER_DISABLED_TABS = ['network', 'pci']
|
||||
|
||||
@ -78,8 +88,46 @@ const Content = ({
|
||||
watch,
|
||||
formState: { errors },
|
||||
control,
|
||||
getValues,
|
||||
reset,
|
||||
} = useFormContext()
|
||||
const { useLoadOsProfile } = useGeneralApi()
|
||||
const [fetchProfile] = useLazyGetOsProfilesQuery()
|
||||
const { view, getResourceView } = useViews()
|
||||
const osProfile = watch('general.OS_PROFILE')
|
||||
const profileIsLoaded =
|
||||
useSelector((state) => state?.general?.loadedOsProfile?.[STEP_ID]) || false
|
||||
|
||||
// Prefill current step based on profile
|
||||
useEffect(async () => {
|
||||
if (!profileIsLoaded && osProfile && osProfile !== '-') {
|
||||
try {
|
||||
const { data: fetchedProfile } = await fetchProfile({ id: osProfile })
|
||||
const currentForm = getValues()
|
||||
const mappedSteps = Object.fromEntries(
|
||||
Object.entries(fetchedProfile).map(([key, value]) => [
|
||||
STEP_MAP[key] || key,
|
||||
value,
|
||||
])
|
||||
)
|
||||
|
||||
const flattenByKeys = Object.keys(TAB_FORM_MAP)
|
||||
mappedSteps.extra = flattenObjectByKeys(
|
||||
mappedSteps.extra,
|
||||
flattenByKeys
|
||||
)
|
||||
|
||||
const mergedSteps = deepmerge(currentForm, mappedSteps)
|
||||
|
||||
reset(mergedSteps, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
keepDefaultValues: true,
|
||||
})
|
||||
useLoadOsProfile({ stepId: STEP_ID })
|
||||
} catch (error) {}
|
||||
}
|
||||
}, [osProfile, profileIsLoaded])
|
||||
|
||||
const hypervisor = useMemo(() => watch(`${GENERAL_ID}.HYPERVISOR`), [])
|
||||
|
||||
|
@ -13,22 +13,36 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useMemo, useEffect } from 'react'
|
||||
import { useMemo, useEffect, useRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useWatch, useFormContext } from 'react-hook-form'
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import useStyles from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/styles'
|
||||
import { useLazyGetOsProfilesQuery } from 'client/features/OneApi/system'
|
||||
|
||||
import {
|
||||
SCHEMA,
|
||||
SECTIONS,
|
||||
} from 'client/components/Forms/VmTemplate/CreateForm/Steps/General/schema'
|
||||
import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper'
|
||||
import { generateKey } from 'client/utils'
|
||||
import { T, RESOURCE_NAMES, VmTemplate } from 'client/constants'
|
||||
import {
|
||||
generateKey,
|
||||
deepmerge,
|
||||
flattenObjectByKeys,
|
||||
sentenceCase,
|
||||
isDevelopment,
|
||||
} from 'client/utils'
|
||||
import {
|
||||
T,
|
||||
RESOURCE_NAMES,
|
||||
VmTemplate,
|
||||
STEP_MAP,
|
||||
TAB_FORM_MAP,
|
||||
} from 'client/constants'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { set } from 'lodash'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
|
||||
let generalFeatures
|
||||
|
||||
@ -40,18 +54,66 @@ const Content = ({
|
||||
adminGroup,
|
||||
setFormData,
|
||||
isVrouter,
|
||||
lastOsProfile,
|
||||
}) => {
|
||||
const [fetchProfile] = useLazyGetOsProfilesQuery()
|
||||
const classes = useStyles()
|
||||
const { view, getResourceView } = useViews()
|
||||
const hypervisor = useWatch({ name: `${STEP_ID}.HYPERVISOR` })
|
||||
|
||||
const { setFieldPath } = useGeneralApi()
|
||||
const { enqueueSuccess, setFieldPath, useResetLoadOsProfile } =
|
||||
useGeneralApi()
|
||||
|
||||
const { getValues, setValue, watch, reset } = useFormContext()
|
||||
const osProfile = watch('general.OS_PROFILE')
|
||||
const currentProfile = useRef(osProfile)
|
||||
|
||||
// Prefill current step based on profile
|
||||
const profileSuccessMsg =
|
||||
Tr(T.Loaded) + ' ' + Tr(T.Profile.toLowerCase()) + ': '
|
||||
|
||||
useEffect(async () => {
|
||||
if (
|
||||
currentProfile.current !== osProfile &&
|
||||
osProfile &&
|
||||
osProfile !== '-'
|
||||
) {
|
||||
try {
|
||||
const { data: fetchedProfile } = await fetchProfile({ id: osProfile })
|
||||
const currentForm = getValues()
|
||||
const mappedSteps = Object.fromEntries(
|
||||
Object.entries(fetchedProfile).map(([key, value]) => [
|
||||
STEP_MAP[key] || key,
|
||||
value,
|
||||
])
|
||||
)
|
||||
const flattenByKeys = Object.keys(TAB_FORM_MAP)
|
||||
mappedSteps.extra = flattenObjectByKeys(
|
||||
mappedSteps.extra,
|
||||
flattenByKeys
|
||||
)
|
||||
|
||||
const mergedSteps = deepmerge(currentForm, mappedSteps)
|
||||
|
||||
reset(mergedSteps, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
keepDefaultValues: true,
|
||||
})
|
||||
|
||||
useResetLoadOsProfile()
|
||||
currentProfile.current = osProfile
|
||||
enqueueSuccess(profileSuccessMsg + sentenceCase(osProfile))
|
||||
} catch (error) {
|
||||
isDevelopment() && console.error('Failed to load profile: ', error)
|
||||
}
|
||||
}
|
||||
}, [osProfile])
|
||||
|
||||
useEffect(() => {
|
||||
setFieldPath(`general`)
|
||||
}, [])
|
||||
|
||||
const { getValues, setValue } = useFormContext()
|
||||
|
||||
// Create watch for vcpu
|
||||
const vcpuWatch = useWatch({
|
||||
name: 'general.VCPU',
|
||||
@ -80,7 +142,15 @@ const Content = ({
|
||||
generalFeatures = features
|
||||
|
||||
return (
|
||||
SECTIONS(hypervisor, isUpdate, features, oneConfig, adminGroup, isVrouter)
|
||||
SECTIONS(
|
||||
hypervisor,
|
||||
isUpdate,
|
||||
features,
|
||||
oneConfig,
|
||||
adminGroup,
|
||||
isVrouter,
|
||||
lastOsProfile
|
||||
)
|
||||
.filter(
|
||||
({ id, required }) => required || sectionsAvailable.includes(id)
|
||||
)
|
||||
@ -119,6 +189,7 @@ const General = ({
|
||||
}) => {
|
||||
const isUpdate = !!vmTemplate?.NAME
|
||||
const initialHypervisor = vmTemplate?.TEMPLATE?.HYPERVISOR
|
||||
const lastOsProfile = vmTemplate?.TEMPLATE?.OS_PROFILE || ''
|
||||
|
||||
return {
|
||||
id: STEP_ID,
|
||||
@ -136,6 +207,7 @@ const General = ({
|
||||
oneConfig,
|
||||
adminGroup,
|
||||
isVrouter,
|
||||
lastOsProfile,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@ -146,6 +218,7 @@ Content.propTypes = {
|
||||
adminGroup: PropTypes.bool,
|
||||
setFormData: PropTypes.func,
|
||||
isVrouter: PropTypes.bool,
|
||||
lastOsProfile: PropTypes.string,
|
||||
}
|
||||
|
||||
export default General
|
||||
|
@ -17,6 +17,7 @@ import { string, boolean } from 'yup'
|
||||
|
||||
import Image from 'client/components/Image'
|
||||
import { useGetTemplateLogosQuery } from 'client/features/OneApi/logo'
|
||||
import { useGetOsProfilesQuery } from 'client/features/OneApi/system'
|
||||
import { Field, arrayToOptions } from 'client/utils'
|
||||
import {
|
||||
T,
|
||||
@ -80,14 +81,11 @@ export const LOGO = {
|
||||
values: () => {
|
||||
const { data: logos } = useGetTemplateLogosQuery()
|
||||
|
||||
return arrayToOptions(
|
||||
[['-', DEFAULT_TEMPLATE_LOGO], ...Object.entries(logos || {})],
|
||||
{
|
||||
addEmpty: false,
|
||||
getText: ([name]) => name,
|
||||
getValue: ([, logo]) => logo,
|
||||
}
|
||||
)
|
||||
return arrayToOptions([...Object.entries(logos || {})], {
|
||||
addEmpty: true,
|
||||
getText: ([name]) => name,
|
||||
getValue: ([, logo]) => logo,
|
||||
})
|
||||
},
|
||||
renderValue: (value) => (
|
||||
<Image
|
||||
@ -111,9 +109,41 @@ export const VROUTER_FIELD = {
|
||||
label: T.MakeTemplateAvailableForVROnly,
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().yesOrNo(),
|
||||
grid: { md: 12 },
|
||||
grid: { md: 6 },
|
||||
}
|
||||
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
export const OS_PROFILE = (isUpdate, lastOsProfile) => ({
|
||||
name: 'OS_PROFILE',
|
||||
label: T.Profile,
|
||||
type: INPUT_TYPES.AUTOCOMPLETE,
|
||||
optionsOnly: true,
|
||||
values: () => {
|
||||
const { data: profiles = [] } = useGetOsProfilesQuery()
|
||||
|
||||
return arrayToOptions(profiles, {
|
||||
addEmpty: true,
|
||||
getText: (val) => {
|
||||
try {
|
||||
const parts = val
|
||||
?.split('_')
|
||||
?.map((part) => part?.charAt(0).toUpperCase() + part?.slice(1))
|
||||
|
||||
return `${val === lastOsProfile ? '*' : ''} ${parts?.join(' ')}`
|
||||
} catch (error) {
|
||||
return val
|
||||
}
|
||||
},
|
||||
getValue: (val) => val,
|
||||
})
|
||||
},
|
||||
validation: string()
|
||||
.trim()
|
||||
.notRequired()
|
||||
.default(() => '-'),
|
||||
grid: { md: 6 },
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {boolean} isUpdate - If `true`, the form is being updated
|
||||
* @returns {Field[]} List of information fields
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
FIELDS as INFORMATION_FIELDS,
|
||||
HYPERVISOR_FIELD,
|
||||
VROUTER_FIELD,
|
||||
OS_PROFILE,
|
||||
} from './informationSchema'
|
||||
import {
|
||||
MEMORY_FIELDS,
|
||||
@ -45,6 +46,7 @@ import { T, HYPERVISORS, VmTemplateFeatures } from 'client/constants'
|
||||
* @param {object} oneConfig - Config of oned.conf
|
||||
* @param {boolean} adminGroup - User is admin or not
|
||||
* @param {boolean} isVrouter - VRouter template
|
||||
* @param {string} lastOsProfile - Last used OS profile
|
||||
* @returns {Section[]} Fields
|
||||
*/
|
||||
const SECTIONS = (
|
||||
@ -53,7 +55,8 @@ const SECTIONS = (
|
||||
features,
|
||||
oneConfig,
|
||||
adminGroup,
|
||||
isVrouter
|
||||
isVrouter,
|
||||
lastOsProfile
|
||||
) =>
|
||||
[
|
||||
{
|
||||
@ -61,7 +64,18 @@ const SECTIONS = (
|
||||
legend: T.Hypervisor,
|
||||
required: true,
|
||||
fields: disableFields(
|
||||
[HYPERVISOR_FIELD(isUpdate), VROUTER_FIELD],
|
||||
[HYPERVISOR_FIELD(isUpdate)],
|
||||
'',
|
||||
oneConfig,
|
||||
adminGroup
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'osprofiles',
|
||||
legend: T.OsProfile,
|
||||
required: true,
|
||||
fields: disableFields(
|
||||
[OS_PROFILE(isUpdate, lastOsProfile), VROUTER_FIELD],
|
||||
'',
|
||||
oneConfig,
|
||||
adminGroup
|
||||
@ -143,6 +157,7 @@ const SECTIONS = (
|
||||
* @param {object} oneConfig - Config of oned.conf
|
||||
* @param {boolean} adminGroup - User is admin or not
|
||||
* @param {boolean} isVrouter - VRouter template
|
||||
* @param {string} lastOsProfile - Last used OS profile
|
||||
* @returns {BaseSchema} Step schema
|
||||
*/
|
||||
const SCHEMA = (
|
||||
@ -151,10 +166,19 @@ const SCHEMA = (
|
||||
features,
|
||||
oneConfig,
|
||||
adminGroup,
|
||||
isVrouter
|
||||
isVrouter,
|
||||
lastOsProfile
|
||||
) =>
|
||||
getObjectSchemaFromFields(
|
||||
SECTIONS(hypervisor, isUpdate, features, oneConfig, adminGroup, isVrouter)
|
||||
SECTIONS(
|
||||
hypervisor,
|
||||
isUpdate,
|
||||
features,
|
||||
oneConfig,
|
||||
adminGroup,
|
||||
isVrouter,
|
||||
lastOsProfile
|
||||
)
|
||||
.map(({ fields }) => fields)
|
||||
.flat()
|
||||
)
|
||||
|
@ -26,6 +26,14 @@ const useStyles = makeStyles((theme) => ({
|
||||
gridTemplateColumns: '1fr',
|
||||
},
|
||||
},
|
||||
osprofiles: {
|
||||
gridColumn: '1 / span 2',
|
||||
gridRow: '2',
|
||||
padding: theme.spacing(1),
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
gridColumn: '1 / -1',
|
||||
},
|
||||
},
|
||||
hypervisor: {
|
||||
gridColumn: '1 / span 2',
|
||||
gridRow: '1',
|
||||
|
@ -124,6 +124,8 @@ const Steps = createSteps([General, ExtraConfiguration, CustomVariables], {
|
||||
|
||||
const defaultType = T.SelectCluster
|
||||
objectSchema[EXTRA_ID].CLUSTER_HOST_TYPE = defaultType
|
||||
// Do not load a initial profile
|
||||
delete objectSchema.general.OS_PROFILE
|
||||
|
||||
const knownTemplate = schema.cast(objectSchema, {
|
||||
stripUnknown: false,
|
||||
|
@ -1220,6 +1220,8 @@ module.exports = {
|
||||
sort the suitable datastores for this VM`,
|
||||
Stripping: 'Stripping',
|
||||
LoadAware: 'Load-aware',
|
||||
Loaded: 'Loaded',
|
||||
Loading: 'Loading',
|
||||
Packing: 'Packing',
|
||||
/* VM Template schema - Backup */
|
||||
BackupVolatileDisksQuestion: 'Backup volatile disks?',
|
||||
@ -1335,6 +1337,7 @@ module.exports = {
|
||||
Inputs: 'Inputs',
|
||||
PciDevices: 'PCI Devices',
|
||||
Profile: 'Profile',
|
||||
OsProfile: 'Operating System Profile',
|
||||
DeviceName: 'Device name',
|
||||
Device: 'Device',
|
||||
DeviceTooltip:
|
||||
|
@ -165,6 +165,12 @@ export const TAB_FORM_MAP = {
|
||||
Backup: ['BACKUP_CONFIG'],
|
||||
}
|
||||
|
||||
export const STEP_MAP = {
|
||||
'Advanced options': 'extra',
|
||||
General: 'general',
|
||||
'Custom Variables': 'custom-variables',
|
||||
}
|
||||
|
||||
/** @enum {string} Methods on IP v4 options type */
|
||||
export const IPV4_METHODS = {
|
||||
[T.Ipv4Static]: 'static',
|
||||
|
@ -14,6 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, useEffect } from 'react'
|
||||
import { STEP_MAP, TAB_FORM_MAP, T } from 'client/constants'
|
||||
import { useStore } from 'react-redux'
|
||||
import { useHistory, useLocation } from 'react-router'
|
||||
|
||||
@ -28,7 +29,9 @@ import { useGetHostsQuery } from 'client/features/OneApi/host'
|
||||
import { useGetImagesQuery } from 'client/features/OneApi/image'
|
||||
import { useGetUsersQuery } from 'client/features/OneApi/user'
|
||||
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
|
||||
import { useLazyGetOsProfilesQuery } from 'client/features/OneApi/system'
|
||||
|
||||
import { deepmerge, isDevelopment } from 'client/utils'
|
||||
import {
|
||||
DefaultFormStepper,
|
||||
SkeletonStepsForm,
|
||||
@ -38,7 +41,6 @@ import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
|
||||
import { jsonToXml } from 'client/models/Helper'
|
||||
import { filterTemplateData, transformActionsCreate } from 'client/utils/parser'
|
||||
import { TAB_FORM_MAP, T } from 'client/constants'
|
||||
|
||||
import { useSystemData } from 'client/features/Auth'
|
||||
|
||||
@ -68,6 +70,7 @@ function CreateVmTemplate() {
|
||||
useGeneralApi()
|
||||
const [update] = useUpdateTemplateMutation()
|
||||
const [allocate] = useAllocateTemplateMutation()
|
||||
const [fetchProfile] = useLazyGetOsProfilesQuery()
|
||||
const { adminGroup, oneConfig } = useSystemData()
|
||||
|
||||
const { data: apiTemplateDataExtended } = useGetTemplateQuery(
|
||||
@ -90,7 +93,35 @@ function CreateVmTemplate() {
|
||||
try {
|
||||
// Get current state and modified fields
|
||||
const currentState = store.getState()
|
||||
const modifiedFields = currentState.general?.modifiedFields
|
||||
const osProfile = rawTemplate?.general?.OS_PROFILE
|
||||
let modifiedFields = currentState.general?.modifiedFields
|
||||
// This loads the OS profile and marks all fields of it as modified so they wont be filtered out
|
||||
if (osProfile && osProfile !== '-') {
|
||||
try {
|
||||
const convertLeafValuesToBoolean = (obj) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(obj).map(([key, value]) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return [key, convertLeafValuesToBoolean(value)]
|
||||
}
|
||||
|
||||
return [key, value != null]
|
||||
})
|
||||
)
|
||||
|
||||
const { data: fetchedProfile } = await fetchProfile({ id: osProfile })
|
||||
const mappedSteps = Object.fromEntries(
|
||||
Object.entries(fetchedProfile).map(([step, values]) => [
|
||||
STEP_MAP[step] || step,
|
||||
convertLeafValuesToBoolean(values),
|
||||
])
|
||||
)
|
||||
modifiedFields = deepmerge(modifiedFields, mappedSteps)
|
||||
} catch (error) {
|
||||
isDevelopment() &&
|
||||
console.error('Failed mapping profile filter: ', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the original template
|
||||
const existingTemplate = {
|
||||
|
@ -23,6 +23,8 @@ export const setSelectedIds = createAction('Set selected IDs')
|
||||
export const setUpdateDialog = createAction('Set update dialog')
|
||||
|
||||
export const updateDisabledSteps = createAction('Set disabled steps')
|
||||
export const setLoadOsProfile = createAction('Set load OS profile')
|
||||
export const resetLoadOsProfile = createAction('Reset load OS profiles')
|
||||
export const dismissSnackbar = createAction('Dismiss snackbar')
|
||||
export const deleteSnackbar = createAction('Delete snackbar')
|
||||
export const setUploadSnackbar = createAction('Change upload snackbar')
|
||||
|
@ -47,6 +47,14 @@ export const useGeneralApi = () => {
|
||||
},
|
||||
resetModifiedFields: () => dispatch(actions.resetModifiedFields()),
|
||||
|
||||
useLoadOsProfile: (stepId) => {
|
||||
dispatch(actions.setLoadOsProfile(stepId))
|
||||
},
|
||||
|
||||
useResetLoadOsProfile: () => {
|
||||
dispatch(actions.resetLoadOsProfile())
|
||||
},
|
||||
|
||||
// dismiss all if no key has been defined
|
||||
dismissSnackbar: (key) =>
|
||||
dispatch(actions.dismissSnackbar({ key, dismissAll: !key })),
|
||||
|
@ -30,6 +30,7 @@ const initial = {
|
||||
isLoading: false,
|
||||
isFixMenu: false,
|
||||
isUpdateDialog: false,
|
||||
loadedOsProfile: {},
|
||||
upload: 0,
|
||||
notifications: [],
|
||||
selectedIds: [],
|
||||
@ -85,6 +86,19 @@ const slice = createSlice({
|
||||
state.isUpdateDialog = !!payload
|
||||
})
|
||||
|
||||
.addCase(actions.setLoadOsProfile, (state, { payload }) => {
|
||||
const { stepId } = payload
|
||||
const exists = get(state.loadedOsProfile, stepId, false)
|
||||
|
||||
if (!exists) {
|
||||
set(state.loadedOsProfile, stepId, true)
|
||||
}
|
||||
})
|
||||
|
||||
.addCase(actions.resetLoadOsProfile, (state) => {
|
||||
state.loadedOsProfile = {}
|
||||
})
|
||||
|
||||
/* FIELD MODIFICATIONS */
|
||||
.addCase(actions.setFieldPath, (state, { payload }) => {
|
||||
state.fieldPath = payload
|
||||
|
@ -15,8 +15,8 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { Actions, Commands } from 'server/utils/constants/commands/system'
|
||||
import {
|
||||
Actions as VmmActions,
|
||||
Commands as VmmCommands,
|
||||
Actions as SystemActions,
|
||||
Commands as SystemCommands,
|
||||
} from 'server/routes/api/system/routes'
|
||||
import {
|
||||
Actions as SunstoneActions,
|
||||
@ -122,6 +122,30 @@ const systemApi = oneApi.injectEndpoints({
|
||||
keepUnusedDataFor: 600,
|
||||
}),
|
||||
|
||||
getOsProfiles: builder.query({
|
||||
/**
|
||||
* Returns the OS Profiles or a specific profile's content.
|
||||
*
|
||||
* @param {object} params - Request params
|
||||
* @returns {object} The set config options
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
query: (params) => {
|
||||
const name = SystemActions.PROFILES
|
||||
const command = { name, ...SystemCommands[name] }
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
providesTags: (response) => {
|
||||
if (Array.isArray(response) && response?.length > 0) {
|
||||
return response?.map((profile) => ({ type: SYSTEM, id: profile }))
|
||||
} else {
|
||||
return [{ type: SYSTEM, id: 'os_profile' }]
|
||||
}
|
||||
},
|
||||
keepUnusedDataFor: 600,
|
||||
}),
|
||||
|
||||
getVmmConfig: builder.query({
|
||||
/**
|
||||
* Returns the hypervisor VMM_EXEC config.
|
||||
@ -131,8 +155,8 @@ const systemApi = oneApi.injectEndpoints({
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
query: (params) => {
|
||||
const name = VmmActions.VMM_CONFIG
|
||||
const command = { name, ...VmmCommands[name] }
|
||||
const name = SystemActions.VMM_CONFIG
|
||||
const command = { name, ...SystemCommands[name] }
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
@ -150,6 +174,8 @@ export const {
|
||||
useLazyGetOneConfigQuery,
|
||||
useGetVmmConfigQuery,
|
||||
useLazyGetVmmConfigQuery,
|
||||
useGetOsProfilesQuery,
|
||||
useLazyGetOsProfilesQuery,
|
||||
useGetSunstoneConfigQuery,
|
||||
useLazyGetSunstoneConfigQuery,
|
||||
useGetSunstoneViewsQuery,
|
||||
|
@ -127,3 +127,30 @@ deepmerge.all = function deepmergeAll(array, options) {
|
||||
return deepmerge(prev, next, options)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens specified keys in an object by one level without mutating the original object.
|
||||
*
|
||||
* @param {object} obj - The object to flatten.
|
||||
* @param {Array} keysToFlatten - The keys whose nested objects should be flattened.
|
||||
* @returns {object} The new flattened object.
|
||||
*/
|
||||
export const flattenObjectByKeys = (obj, keysToFlatten) => {
|
||||
let result = { ...obj }
|
||||
|
||||
keysToFlatten.forEach((key) => {
|
||||
if (
|
||||
key in result &&
|
||||
typeof result[key] === 'object' &&
|
||||
result[key] !== null
|
||||
) {
|
||||
result = {
|
||||
...result,
|
||||
...result[key],
|
||||
}
|
||||
delete result[key]
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
@ -34,19 +34,19 @@ import {
|
||||
import parsePayload from 'client/utils/parser/parseTemplatePayload'
|
||||
|
||||
export {
|
||||
templateToObject,
|
||||
convertKeysToCase,
|
||||
filterTemplateData,
|
||||
isDeeplyEmpty,
|
||||
parseAcl,
|
||||
parseApplicationToForm,
|
||||
parseCustomInputString,
|
||||
parseFormToApplication,
|
||||
parseFormToDeployApplication,
|
||||
parseAcl,
|
||||
parseNetworkString,
|
||||
parseCustomInputString,
|
||||
convertKeysToCase,
|
||||
parseVmTemplateContents,
|
||||
parseTouchedDirty,
|
||||
isDeeplyEmpty,
|
||||
filterTemplateData,
|
||||
parsePayload,
|
||||
parseTouchedDirty,
|
||||
parseVmTemplateContents,
|
||||
templateToObject,
|
||||
transformActionsCreate,
|
||||
transformActionsInstantiate,
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ const {
|
||||
} = require('server/utils/constants/commands/system')
|
||||
const { createTokenServerAdmin } = require('server/routes/api/auth/utils')
|
||||
const { getVmmConfig } = require('server/utils/vmm')
|
||||
const { getProfiles } = require('server/utils/profiles')
|
||||
|
||||
const { defaultEmptyFunction, httpMethod } = defaults
|
||||
const { ok, internalServerError, badRequest, notFound } = httpCodes
|
||||
@ -163,7 +164,61 @@ const getVmmConfigHandler = async (
|
||||
next()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} res - http response
|
||||
* @param {Function} next - express stepper
|
||||
* @param {object} params - params of http request
|
||||
* @param {object} [params.id="-1"] - fetch [id].yaml profile
|
||||
* @returns {void}
|
||||
*/
|
||||
const getTemplateProfiles = async (
|
||||
res,
|
||||
next = defaultEmptyFunction,
|
||||
params = {}
|
||||
) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const fetchAll = id === '-1'
|
||||
const foundProfiles = await getProfiles(id)
|
||||
|
||||
if (fetchAll) {
|
||||
if (!Array.isArray(foundProfiles) || foundProfiles.length === 0) {
|
||||
;(res.locals ??= {}).httpCode = httpResponse(
|
||||
notFound,
|
||||
'No OS profiles found',
|
||||
''
|
||||
)
|
||||
|
||||
return next()
|
||||
}
|
||||
} else {
|
||||
if (!foundProfiles || Object.keys(foundProfiles).length === 0) {
|
||||
;(res.locals ??= {}).httpCode = httpResponse(
|
||||
notFound,
|
||||
'OS profile not found',
|
||||
''
|
||||
)
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
;(res.locals ??= {}).httpCode = httpResponse(ok, foundProfiles)
|
||||
} catch (error) {
|
||||
const httpError = httpResponse(
|
||||
internalServerError,
|
||||
error?.message || 'Error loading OS profiles',
|
||||
''
|
||||
)
|
||||
writeInLogger(httpError)
|
||||
;(res.locals ??= {}).httpCode = httpError
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
module.exports = {
|
||||
getConfig,
|
||||
getVmmConfigHandler,
|
||||
getTemplateProfiles,
|
||||
}
|
||||
|
@ -18,9 +18,10 @@ const { Actions, Commands } = require('server/routes/api/system/routes')
|
||||
const {
|
||||
getConfig,
|
||||
getVmmConfigHandler,
|
||||
getTemplateProfiles,
|
||||
} = require('server/routes/api/system/functions')
|
||||
|
||||
const { SYSTEM_CONFIG, VMM_CONFIG } = Actions
|
||||
const { SYSTEM_CONFIG, VMM_CONFIG, PROFILES } = Actions
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
@ -31,4 +32,8 @@ module.exports = [
|
||||
...Commands[VMM_CONFIG],
|
||||
action: getVmmConfigHandler,
|
||||
},
|
||||
{
|
||||
...Commands[PROFILES],
|
||||
action: getTemplateProfiles,
|
||||
},
|
||||
]
|
||||
|
@ -24,9 +24,12 @@ const { GET } = httpMethod
|
||||
|
||||
const SYSTEM_CONFIG = 'system.config'
|
||||
const VMM_CONFIG = 'vmm.config'
|
||||
const PROFILES = 'system.profiles'
|
||||
|
||||
const Actions = {
|
||||
SYSTEM_CONFIG,
|
||||
VMM_CONFIG,
|
||||
PROFILES,
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@ -48,5 +51,16 @@ module.exports = {
|
||||
},
|
||||
auth: true,
|
||||
},
|
||||
[PROFILES]: {
|
||||
path: `${basepath}/profiles`,
|
||||
httpMethod: GET,
|
||||
params: {
|
||||
id: {
|
||||
from: query,
|
||||
default: '-1',
|
||||
},
|
||||
},
|
||||
auth: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
94
src/fireedge/src/server/utils/profiles.js
Normal file
94
src/fireedge/src/server/utils/profiles.js
Normal file
@ -0,0 +1,94 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2024, 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const { global } = require('window-or-global')
|
||||
const { parse } = require('yaml')
|
||||
|
||||
/**
|
||||
* Parse profile configuration.
|
||||
*
|
||||
* @param {string} fileContent - Content of the configuration file.
|
||||
* @returns {object} Parsed os profile object.
|
||||
*/
|
||||
const parseProfileFileContent = (fileContent) => {
|
||||
try {
|
||||
return parse(fileContent)
|
||||
} catch (error) {
|
||||
throw new Error(`Error parsing profile: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of an OS template profile or list all profiles.
|
||||
*
|
||||
* @param {string} [id='-1'] - The name of the profile. If '-1' or undefined, list all profiles.
|
||||
* @returns {Promise<object|string[]>} Parsed os profile object or list of profile names.
|
||||
*/
|
||||
const getProfiles = async (id = '-1') => {
|
||||
const vmTemplateProfilesDir = global?.paths?.OS_PROFILES
|
||||
|
||||
if (!vmTemplateProfilesDir) {
|
||||
throw new Error('OS_PROFILES path is not defined.')
|
||||
}
|
||||
|
||||
let profileFilePath
|
||||
|
||||
if (id && id !== '-1') {
|
||||
profileFilePath = path.join(vmTemplateProfilesDir, `${id}.yaml`)
|
||||
} else {
|
||||
profileFilePath = vmTemplateProfilesDir
|
||||
}
|
||||
|
||||
const exists = await fs.pathExists(profileFilePath)
|
||||
|
||||
if (!exists) {
|
||||
if (id && id !== '-1') {
|
||||
throw new Error('OS profile not found')
|
||||
} else {
|
||||
throw new Error('No OS profiles found')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(profileFilePath)
|
||||
|
||||
if (stats.isFile()) {
|
||||
const fileContent = await fs.readFile(profileFilePath, 'utf-8')
|
||||
|
||||
return parseProfileFileContent(fileContent)
|
||||
} else if (stats.isDirectory()) {
|
||||
const profiles = await fs.readdir(profileFilePath)
|
||||
const yamlFiles = profiles
|
||||
.filter((file) => path.extname(file) === '.yaml')
|
||||
.map((file) => path.basename(file, '.yaml'))
|
||||
|
||||
if (yamlFiles.length === 0) {
|
||||
throw new Error('No OS profiles found')
|
||||
}
|
||||
|
||||
return yamlFiles
|
||||
} else {
|
||||
throw new Error(`Unknown file type: ${profileFilePath}`)
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`${error?.message || 'OS Profile error'}`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getProfiles,
|
||||
}
|
@ -584,6 +584,9 @@ const genPathResources = () => {
|
||||
if (!global.paths.VMM_EXEC_CONFIG) {
|
||||
global.paths.VMM_EXEC_CONFIG = `${ETC_LOCATION}/vmm_exec`
|
||||
}
|
||||
if (!global.paths.OS_PROFILES) {
|
||||
global.paths.OS_PROFILES = `${ETC_LOCATION}/${defaultSunstonePath}/profiles`
|
||||
}
|
||||
if (!global.paths.FIREEDGE_KEY_PATH) {
|
||||
global.paths.FIREEDGE_KEY_PATH = `${VAR_LOCATION}/.one/${defaultKeyFilename}`
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user