F OpennNebula/one#6880: Update backup configuration on a virtual machine (#3540)

Signed-off-by: Ángel Cívico Martos <acivico@opennnebula.io>
This commit is contained in:
Angel authored and GitHub committed 2025-04-03 17:21:09 +02:00
1 parent 03fb9cc10c
commit 33db8abce7
10 files changed
+162 -85

No files matched your search

@@ -13,27 +13,21 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import { Stack } from '@mui/material'
import { FormWithSchema } from 'client/components/Forms'
import { SECTIONS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/backup/schema'
import { T } from 'client/constants'
import PropTypes from 'prop-types'
import { ReactElement } from 'react'
/**
* @param {object} props - Component props
* @param {object} props - Component properties
* @param {object} props.oneConfig - OpenNEbula configuration
* @param {boolean} props.adminGroup - If the user is admin
* @returns {ReactElement} IO section component
* @returns {ReactElement} Form content component
*/
const Backup = ({ oneConfig, adminGroup }) => (
<Stack
display="grid"
gap="1em"
sx={{ gridTemplateColumns: { sm: '1fr', md: '1fr 1fr' } }}
>
const Content = ({ oneConfig, adminGroup }) => (
<Stack display="grid" gap="1em" sx={{ gridTemplateColumns: '1fr' }}>
{SECTIONS(oneConfig, adminGroup).map(({ id, ...section }) => (
<FormWithSchema
key={id}
@@ -45,11 +39,9 @@ const Backup = ({ oneConfig, adminGroup }) => (
</Stack>
)
Backup.displayName = 'Backup'
Backup.propTypes = {
Content.propTypes = {
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
export default Backup
export default Content
@@ -0,0 +1,52 @@
/* ------------------------------------------------------------------------- *
* 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. *
* ------------------------------------------------------------------------- */
/* ------------------------------------------------------------------------- *
* Copyright 2002-2025, 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. *
* ------------------------------------------------------------------------- */
import ContentForm from 'client/components/Forms/Vm/BackupConfigForm/content'
import { SCHEMA } from 'client/components/Forms/Vm/BackupConfigForm/schema'
import { createForm } from 'client/utils'
import PropTypes from 'prop-types'
import { ReactElement } from 'react'
/**
* @param {object} props - Component props
* @param {object} props.oneConfig - OpenNEbula configuration
* @param {boolean} props.adminGroup - If the user is admin
* @returns {ReactElement} IO section component
*/
const BackupConfigForm = createForm(SCHEMA, undefined, { ContentForm })
BackupConfigForm.displayName = 'BackupConfigForm'
BackupConfigForm.propTypes = {
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
export default BackupConfigForm
@@ -13,24 +13,22 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, useMemo } from 'react'
import PropTypes from 'prop-types'
import { useFormContext } from 'react-hook-form'
import {
SystemShut as OsIcon,
DataTransferBoth as IOIcon,
Folder as ContextIcon,
RefreshDouble as BackupIcon,
DataTransferBoth as IOIcon,
SystemShut as OsIcon,
} from 'iconoir-react'
import PropTypes from 'prop-types'
import { ReactElement, useMemo } from 'react'
import { useFormContext } from 'react-hook-form'
import InputOutput from 'client/components/Forms/Vm/UpdateConfigurationForm/inputOutput'
import Booting from 'client/components/Forms/Vm/UpdateConfigurationForm/booting'
import Context from 'client/components/Forms/Vm/UpdateConfigurationForm/context'
import Backup from 'client/components/Forms/Vm/UpdateConfigurationForm/backup'
import InputOutput from 'client/components/Forms/Vm/UpdateConfigurationForm/inputOutput'
import Tabs from 'client/components/Tabs'
import { Translate } from 'client/components/HOC'
import { T, HYPERVISORS } from 'client/constants'
import Tabs from 'client/components/Tabs'
import { HYPERVISORS, T } from 'client/constants'
/**
* @param {object} props - Component props
@@ -87,17 +85,6 @@ const Content = ({ hypervisor, oneConfig, adminGroup, vm }) => {
),
error: !!errors?.CONTEXT,
},
{
id: 'backup',
icon: BackupIcon,
label: <Translate word={T.Backup} />,
renderContent: () => (
<Backup oneConfig={oneConfig} adminGroup={adminGroup} />
),
error: ['BACKUP_VOLATILE', 'FS_FREEZE', 'KEEP_LAST', 'MODE'].some(
(id) => errors?.[`BACKUP_CONFIG.${id}`]
),
},
],
[errors, hypervisor]
)
@@ -15,22 +15,21 @@
* ------------------------------------------------------------------------- */
import { reach } from 'yup'
import { set } from 'lodash'
import { SCHEMA } from 'client/components/Forms/Vm/UpdateConfigurationForm/schema'
import ContentForm from 'client/components/Forms/Vm/UpdateConfigurationForm/content'
import { SCHEMA } from 'client/components/Forms/Vm/UpdateConfigurationForm/schema'
import {
createForm,
decodeBase64,
getUnknownAttributes,
isBase64,
decodeBase64,
} from 'client/utils'
import { set } from 'lodash'
const UpdateConfigurationForm = createForm(SCHEMA, undefined, {
ContentForm,
transformInitialValue: (vmTemplate, schema) => {
const template = vmTemplate?.TEMPLATE ?? {}
const context = template?.CONTEXT ?? {}
const backupConfig = vmTemplate?.BACKUPS?.BACKUP_CONFIG ?? {}
const bootOrder = template?.OS?.BOOT
const nics = [].concat(template?.NIC ?? []).flat()
const knownTemplate = schema.cast(
@@ -44,15 +43,6 @@ const UpdateConfigurationForm = createForm(SCHEMA, undefined, {
context: { ...template },
})
// Get the custom vars from the context
const knownBackupConfig = reach(schema, 'BACKUP_CONFIG').cast(
backupConfig,
{
stripUnknown: true,
context: { ...template },
}
)
// Merge known and unknown context custom vars
knownTemplate.CONTEXT = {
...knownContext,
@@ -68,12 +58,6 @@ const UpdateConfigurationForm = createForm(SCHEMA, undefined, {
}
}
// Merge known and unknown context custom vars
knownTemplate.BACKUP_CONFIG = {
...knownBackupConfig,
...getUnknownAttributes(backupConfig, knownBackupConfig),
}
// Easy compatibility with the bootOrder component by specifying the same form paths as in the VM Template
!!bootOrder && set(knownTemplate, 'extra.OS.BOOT', bootOrder)
!!template?.DISK &&
@@ -17,9 +17,8 @@ import { object, ObjectSchema } from 'yup'
import { HYPERVISORS } from 'client/constants'
import { SCHEMA as OS_SCHEMA } from './booting/schema'
import { SCHEMA as IO_SCHEMA } from './inputOutput/schema'
import { SCHEMA as CONTEXT_SCHEMA } from './context/schema'
import { SCHEMA as BACKUP_SCHEMA } from './backup/schema'
import { SCHEMA as IO_SCHEMA } from './inputOutput/schema'
/**
* @param {object} [formProps] - Form props
@@ -31,6 +30,5 @@ export const SCHEMA = ({ hypervisor }) =>
.concat(IO_SCHEMA({ hypervisor }))
.concat(OS_SCHEMA({ hypervisor }))
.concat(CONTEXT_SCHEMA({ hypervisor }))
.concat(BACKUP_SCHEMA())
export { IO_SCHEMA, OS_SCHEMA, CONTEXT_SCHEMA, BACKUP_SCHEMA }
export { CONTEXT_SCHEMA, IO_SCHEMA, OS_SCHEMA }
@@ -194,6 +194,13 @@ const UpdateConfigurationForm = (configProps) =>
const BackupForm = (configProps) =>
AsyncLoadForm({ formPath: 'Vm/BackupForm' }, configProps)
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
*/
const BackupConfigForm = (configProps) =>
AsyncLoadForm({ formPath: 'Vm/BackupConfigForm' }, configProps)
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
@@ -202,9 +209,12 @@ const AttachPciForm = (configProps) =>
AsyncLoadForm({ formPath: 'Vm/AttachPciForm' }, configProps)
export {
AliasForm,
AttachAliasForm,
AttachNicForm,
AttachPciForm,
AttachSecGroupForm,
BackupConfigForm,
BackupForm,
ChangeGroupForm,
ChangeUserForm,
@@ -225,6 +235,4 @@ export {
UpdateConfigurationForm,
UpdateNicForm,
VolatileSteps,
AliasForm,
AttachAliasForm,
}
@@ -13,23 +13,23 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { ReactElement } from 'react'
import { generatePath } from 'react-router-dom'
import {
useRenameImageMutation,
useChangeImageTypeMutation,
usePersistentImageMutation,
} from 'client/features/OneApi/image'
import { StatusChip } from 'client/components/Status'
import { List } from 'client/components/Tabs/Common'
import {
useChangeImageTypeMutation,
usePersistentImageMutation,
useRenameImageMutation,
} from 'client/features/OneApi/image'
import { getDiskType, getType, getState } from 'client/models/Image'
import { timeToString, booleanToString } from 'client/models/Helper'
import { arrayToOptions, prettyBytes } from 'client/utils'
import { T, Image, IMAGE_ACTIONS, IMAGE_TYPES } from 'client/constants'
import { PATH } from 'client/apps/sunstone/routesOne'
import { Image, IMAGE_ACTIONS, IMAGE_TYPES, T } from 'client/constants'
import { booleanToString, timeToString } from 'client/models/Helper'
import { getDiskType, getState, getType } from 'client/models/Image'
import { arrayToOptions, prettyBytes } from 'client/utils'
/**
* Renders mainly information tab.
@@ -13,44 +13,99 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { useGetVmQuery } from 'client/features/OneApi/vm'
import { BackupsTable } from 'client/components/Tables'
import { useHistory, generatePath } from 'react-router-dom'
import { PATH } from 'client/apps/sunstone/routesOne'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import { BackupsTable } from 'client/components/Tables'
import { T } from 'client/constants'
import {
useGetVmQuery,
useUpdateConfigurationMutation,
} from 'client/features/OneApi/vm'
import PropTypes from 'prop-types'
import { ReactElement } from 'react'
import { generatePath, useHistory } from 'react-router-dom'
import { jsonToXml } from 'client/models/Helper'
import { BackupConfigForm } from 'client/components/Forms/Vm'
import { Stack } from '@mui/material'
/**
* Renders the list of backups from a VM.
*
* @param {object} props - Props
* @param {string} props.id - Virtual Machine id
* @param {object} props.oneConfig - OpenNEbula configuration
* @param {boolean} props.adminGroup - If the user is admin
* @returns {ReactElement} Backups tab
*/
const VmBackupTab = ({ id }) => {
const VmBackupTab = ({ id, oneConfig, adminGroup }) => {
const { data: vm = {}, refetch, isFetching } = useGetVmQuery({ id })
const path = PATH.STORAGE.BACKUPS.DETAIL
const history = useHistory()
const [updateConf] = useUpdateConfigurationMutation()
const handleRowClick = (rowId) => {
history.push(generatePath(path, { id: String(rowId) }))
}
const handleUpdateBackupConf = async (newConfiguration) => {
const xml = jsonToXml(newConfiguration)
await updateConf({ id, template: xml })
}
return (
<BackupsTable
disableRowSelect
disableGlobalSort
refetchVm={refetch}
isFetchingVm={isFetching}
vm={vm}
onRowClick={(row) => handleRowClick(row.ID)}
/>
<>
{/* <Backup oneConfig={oneConfig} adminGroup={adminGroup} /> */}
<Stack
direction="row"
alignItems="center"
justifyContent="start"
gap="1rem"
marginBottom="1rem"
>
<ButtonToTriggerForm
buttonProps={{
'data-cy': 'backup-vm',
label: T.BackupConfigVM,
disabled: isFetching,
}}
options={[
{
dialogProps: {
title: T.BackupConfigVM,
dataCy: 'modal-backup-vm',
},
form: () =>
BackupConfigForm({
stepProps: { oneConfig, adminGroup, vm },
initialValues: vm,
}),
onSubmit: handleUpdateBackupConf,
},
]}
/>
</Stack>
<BackupsTable
disableRowSelect
disableGlobalSort
refetchVm={refetch}
isFetchingVm={isFetching}
vm={vm}
onRowClick={(row) => handleRowClick(row.ID)}
/>
</>
)
}
VmBackupTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
}
VmBackupTab.displayName = 'VmBackupTab'
@@ -1233,6 +1233,7 @@ module.exports = {
/* VM Template schema - Backup */
BackupVolatileDisksQuestion: 'Backup volatile disks?',
BackupVolatile: 'Backup volatile',
BackupConfigVM: 'Backup config',
FSFreeze: 'FS Freeze',
Execution: 'Execution',
HowManyBackupsQuestion: 'How many backups do you want to keep?',