1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-16 22:50:10 +03:00

F #5422: Add vm recover form (#1504)

This commit is contained in:
Sergio Betanzos 2021-10-04 10:52:47 +02:00 committed by GitHub
parent 847ea6d949
commit 745f755760
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 163 additions and 12 deletions

View File

@ -19,11 +19,11 @@ import PropTypes from 'prop-types'
import { TextField } from '@material-ui/core'
import { Controller } from 'react-hook-form'
import { ErrorHelper } from 'client/components/FormControl'
import { ErrorHelper, Tooltip } from 'client/components/FormControl'
import { Tr } from 'client/components/HOC'
const SelectController = memo(
({ control, cy, name, label, multiple, values, error, fieldProps }) => {
({ control, cy, name, label, multiple, values, tooltip, error, fieldProps }) => {
const defaultValue = multiple ? [values?.[0]?.value] : values?.[0]?.value
return (
@ -55,6 +55,11 @@ const SelectController = memo(
margin='dense'
SelectProps={{ native: true, multiple }}
label={Tr(label)}
InputProps={{
startAdornment: tooltip && (
<Tooltip title={tooltip} position='start' />
)
}}
inputProps={{ 'data-cy': cy }}
error={Boolean(error)}
helperText={Boolean(error) && <ErrorHelper label={error?.message} />}
@ -76,7 +81,9 @@ const SelectController = memo(
},
(prevProps, nextProps) =>
prevProps.error === nextProps.error &&
prevProps.values.length === nextProps.values.length
prevProps.values.length === nextProps.values.length &&
prevProps.label === nextProps.label &&
prevProps.tooltip === nextProps.tooltip
)
SelectController.propTypes = {
@ -86,6 +93,7 @@ SelectController.propTypes = {
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
multiple: PropTypes.bool,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
tooltip: PropTypes.string,
error: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.objectOf(PropTypes.any)

View File

@ -19,7 +19,7 @@ import PropTypes from 'prop-types'
import { QuestionMarkCircle } from 'iconoir-react'
import { InputAdornment, Typography, Tooltip } from '@material-ui/core'
const AdornmentWithTooltip = memo(({ title, children }) => (
const AdornmentWithTooltip = memo(({ title, position = 'end', children }) => (
<Tooltip
arrow
placement='bottom'
@ -29,7 +29,7 @@ const AdornmentWithTooltip = memo(({ title, children }) => (
</Typography>
}
>
<InputAdornment position='end' style={{ cursor: 'help' }}>
<InputAdornment position={position} style={{ cursor: 'help' }}>
{children ?? <QuestionMarkCircle size={18} />}
</InputAdornment>
</Tooltip>
@ -37,7 +37,8 @@ const AdornmentWithTooltip = memo(({ title, children }) => (
AdornmentWithTooltip.propTypes = {
title: PropTypes.string,
children: PropTypes.any
children: PropTypes.any,
position: PropTypes.oneOf(['start', 'end'])
}
AdornmentWithTooltip.displayName = 'AdornmentWithTooltip'

View File

@ -49,7 +49,7 @@ const ButtonToTriggerForm = ({
const { onSubmit: handleSubmit, form, isConfirmDialog = false, dialogProps = {} } = Form ?? {}
const formConfig = useMemo(() => form?.() ?? {}, [form])
const { steps, defaultValues, resolver, fields, transformBeforeSubmit } = formConfig
const { steps, defaultValues, resolver, description, fields, transformBeforeSubmit } = formConfig
const handleTriggerSubmit = async formData => {
try {
@ -139,7 +139,10 @@ const ButtonToTriggerForm = ({
onSubmit={handleTriggerSubmit}
/>
) : (
<FormWithSchema cy='form-dg' fields={fields} />
<>
{description}
<FormWithSchema cy='form-dg' fields={fields} />
</>
)}
</DialogForm>
)

View File

@ -0,0 +1,41 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 { styled } from '@material-ui/core'
import { createForm } from 'client/utils'
import { SCHEMA, FIELDS } from 'client/components/Forms/Vm/RecoverForm/schema'
const Description = styled('p')(({ theme }) => ({
...theme.typography.subtitle1,
paddingInline: '1rem'
}))
const RecoverForm = createForm(
SCHEMA,
FIELDS,
{
description: (
<Description>
{`Recovers a stuck VM that is waiting for a driver operation.
The recovery may be done by failing, succeeding or retrying the
current operation. YOU NEED TO MANUALLY CHECK THE VM STATUS ON THE HOST,
to decide if the operation was successful or not, or if it can be retried.`}
</Description>
)
}
)
export default RecoverForm

View File

@ -0,0 +1,51 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { string, object } from 'yup'
import { INPUT_TYPES } from 'client/constants'
import { getValidationFromFields } from 'client/utils'
const OPERATION = {
name: 'operation',
label: 'Operation',
type: INPUT_TYPES.SELECT,
dependOf: 'operation',
tooltip: operation => ({
0: 'Recover a VM by failing the pending action',
1: 'Recover a VM by succeeding the pending action',
2: 'Recover a VM by retrying the last failed action',
3: 'No recover action possible, delete the VM',
4: 'No recover action possible, delete and recreate the VM',
5: `No recover action possible, delete the VM from the DB.
It does not trigger any action on the hypervisor`
}[operation]),
values: [
{ text: 'Failure', value: 0 },
{ text: 'Success', value: 1 },
{ text: 'Retry', value: 2 },
{ text: 'Delete', value: 3 },
{ text: 'Recreate', value: 4 },
{ text: 'Delete database', value: 5 }
],
validation: string()
.trim()
.required('Recover operation field is required')
.default(() => 2)
}
export const FIELDS = [OPERATION]
export const SCHEMA = object(getValidationFromFields(FIELDS))

View File

@ -16,6 +16,7 @@
import AttachNicForm from 'client/components/Forms/Vm/AttachNicForm'
import CreateDiskSnapshotForm from 'client/components/Forms/Vm/CreateDiskSnapshotForm'
import CreateSnapshotForm from 'client/components/Forms/Vm/CreateSnapshotForm'
import RecoverForm from 'client/components/Forms/Vm/RecoverForm'
import ResizeCapacityForm from 'client/components/Forms/Vm/ResizeCapacityForm'
import ResizeDiskForm from 'client/components/Forms/Vm/ResizeDiskForm'
import SaveAsDiskForm from 'client/components/Forms/Vm/SaveAsDiskForm'
@ -26,6 +27,7 @@ export {
AttachNicForm,
CreateDiskSnapshotForm,
CreateSnapshotForm,
RecoverForm,
ResizeCapacityForm,
ResizeDiskForm,
SaveAsDiskForm

View File

@ -33,7 +33,7 @@ import { useAuth } from 'client/features/Auth'
import { useVmApi } from 'client/features/One'
import { Tr, Translate } from 'client/components/HOC'
// import { } from 'client/components/Forms/Vm'
import { RecoverForm } from 'client/components/Forms/Vm'
import { createActions } from 'client/components/Tables/Enhanced/Utils'
import { PATH } from 'client/apps/sunstone/routesOne'
import { T, VM_ACTIONS, MARKETPLACE_APP_ACTIONS, VM_ACTIONS_BY_STATE } from 'client/constants'
@ -85,6 +85,7 @@ const Actions = () => {
resume,
resched,
unresched,
recover,
lock,
unlock
} = useVmApi()
@ -330,8 +331,23 @@ const Actions = () => {
accessor: VM_ACTIONS.RECOVER,
name: T.Recover,
disabled: isDisabled(VM_ACTIONS.RECOVER),
isConfirmDialog: true,
onSubmit: () => undefined
dialogProps: {
title: rows => {
const isMultiple = rows?.length > 1
const { ID, NAME } = rows?.[0]?.original
return [
Tr(isMultiple ? T.RecoverSeveralVMs : T.Recover),
!isMultiple && `#${ID} ${NAME}`
].filter(Boolean).join(' - ')
}
},
form: RecoverForm,
onSubmit: async (_, rows) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map(id => recover(id)))
ids?.length > 1 && await Promise.all(ids.map(id => getVm(id)))
}
}]
},
{

View File

@ -66,6 +66,7 @@ module.exports = {
Reboot: 'Reboot',
RebootHard: 'Reboot hard',
Recover: 'Recover',
RecoverSeveralVMs: 'Recover several VMs',
Refresh: 'Refresh',
Release: 'Release',
Remove: 'Remove',

View File

@ -85,3 +85,4 @@ export const deleteSnapshot = createAction(`${VM}/delete/snapshot`, vmService.de
export const addScheduledAction = createAction(`${VM}/add/scheduled-action`, vmService.addScheduledAction)
export const updateScheduledAction = createAction(`${VM}/update/scheduled-action`, vmService.updateScheduledAction)
export const deleteScheduledAction = createAction(`${VM}/delete/scheduled-action`, vmService.deleteScheduledAction)
export const recover = createAction(`${VM}/recover`, vmService.recover)

View File

@ -84,6 +84,7 @@ export const useVmApi = () => {
updateScheduledAction: (id, data) =>
unwrapDispatch(actions.updateScheduledAction({ id, ...data })),
deleteScheduledAction: (id, data) =>
unwrapDispatch(actions.deleteScheduledAction({ id, ...data }))
unwrapDispatch(actions.deleteScheduledAction({ id, ...data })),
recover: (id, operation) => unwrapDispatch(actions.recover({ id, operation }))
}
}

View File

@ -568,6 +568,32 @@ export const vmService = ({
if (!res?.id || res?.id !== httpCodes.ok.id) throw res?.data
return res?.data
},
/**
* Recovers a stuck VM that is waiting for a driver operation.
* The recovery may be done by failing or succeeding the pending operation.
*
* You need to manually check the vm status on the host, to decide
* if the operation was successful or not.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Virtual machine id
* @param {0|1|2|3|4} params.operation - Recover operation:
* success (1), failure (0), retry (2), delete (3), delete-recreate (4)
* @returns {number} Virtual machine id
* @throws Fails when response isn't code 200
*/
recover: async params => {
const name = Actions.VM_RECOVER
const command = { name, ...Commands[name] }
const config = requestConfig(params, command)
const res = await RestClient.request(config)
if (!res?.id || res?.id !== httpCodes.ok.id) throw res?.data
return res?.data
}
})