1
0
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:
vichansson 2024-10-18 10:59:25 +03:00 committed by GitHub
parent 60bfce4146
commit 48f508ea69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 643 additions and 52 deletions

View File

@ -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 \

View 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"

View File

@ -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

View File

@ -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

View File

@ -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(() => []),
}

View File

@ -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`), [])

View File

@ -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

View File

@ -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

View File

@ -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()
)

View File

@ -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',

View File

@ -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,

View File

@ -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:

View File

@ -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',

View File

@ -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 = {

View File

@ -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')

View File

@ -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 })),

View File

@ -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

View File

@ -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,

View File

@ -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
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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,
},
]

View File

@ -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,
},
},
}

View 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,
}

View File

@ -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}`
}