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

M #-: Schedule actions tab on services. Perform action on role. (#3206)

Signed-off-by: dcarracedo <dcarracedo@opennebula.io>
This commit is contained in:
David 2024-08-23 13:08:14 +02:00 committed by GitHub
parent 92541b6da6
commit 5dff9383fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 433 additions and 53 deletions

View File

@ -53,13 +53,14 @@ info-tabs:
log:
enabled: true
scheduler_actions:
sched_actions:
enabled: true
actions:
sched_action_create: true
sched_action_update: true
sched_action_delete: true
sched-add: true
sched-update: false
sched-delete: false
charter_create: true
perform_action: true
# Dialogs

View File

@ -54,13 +54,14 @@ info-tabs:
log:
enabled: true
scheduler_actions:
enabled: false
sched_actions:
enabled: true
actions:
sched_action_create: false
sched_action_update: false
sched_action_delete: false
charter_create: false
sched-add: true
sched-update: false
sched-delete: false
charter_create: true
perform_action: true
# Dialogs

View File

@ -54,13 +54,14 @@ info-tabs:
log:
enabled: true
scheduler_actions:
sched_actions:
enabled: true
actions:
sched_action_create: true
sched_action_update: true
sched_action_delete: true
sched-add: true
sched-update: false
sched-delete: false
charter_create: true
perform_action: true
# Dialogs

View File

@ -26,6 +26,8 @@ import {
CreateSchedActionForm,
} from 'client/components/Forms/Vm'
import { CreatePerformAction } from 'client/components/Forms/Service'
import { Tr, Translate } from 'client/components/HOC'
import {
SERVER_CONFIG,
@ -226,6 +228,45 @@ const CharterButton = memo(({ relative, onSubmit }) => {
)
})
/**
* Returns a button to trigger form to perform an action.
*
* @param {object} props - Props
* @param {object} props.service - Service resource
* @param {boolean} [props.relative] - Applies to the form relative format
* @param {function():Promise} props.onSubmit - Submit function
* @returns {ReactElement} Button
*/
const PerformActionButton = memo(
({ service, onSubmit, oneConfig, adminGroup, roles }) => {
const formConfig = {
stepProps: { service, oneConfig, adminGroup, roles },
}
return (
<ButtonToTriggerForm
buttonProps={{
color: 'secondary',
'data-cy': VM_ACTIONS.PERFORM_ACTION,
label: T.PerformAction,
variant: 'outlined',
}}
options={[
{
name: T.PerformAction,
dialogProps: {
title: T.PerformAction,
dataCy: 'modal-perform-action',
},
form: () => CreatePerformAction(formConfig),
onSubmit,
},
]}
/>
)
}
)
const ButtonPropTypes = {
vm: PropTypes.object,
relative: PropTypes.bool,
@ -234,6 +275,8 @@ const ButtonPropTypes = {
oneConfig: PropTypes.object,
adminGroup: PropTypes.bool,
backupjobs: PropTypes.bool,
service: PropTypes.object,
roles: PropTypes.object,
}
CreateSchedButton.propTypes = ButtonPropTypes
@ -244,10 +287,13 @@ DeleteSchedButton.propTypes = ButtonPropTypes
DeleteSchedButton.displayName = 'DeleteSchedButton'
CharterButton.propTypes = ButtonPropTypes
CharterButton.displayName = 'CharterButton'
PerformActionButton.propTypes = ButtonPropTypes
PerformActionButton.displayName = 'PerformActionButton'
export {
CharterButton,
CreateSchedButton,
DeleteSchedButton,
UpdateSchedButton,
PerformActionButton,
}

View File

@ -0,0 +1,38 @@
/* ------------------------------------------------------------------------- *
* 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. *
* ------------------------------------------------------------------------- */
import { createForm } from 'client/utils'
import {
FIELDS,
SCHEMA,
} from 'client/components/Forms/Service/PerformAction/schema'
const PerformActionForm = createForm(SCHEMA, FIELDS, {
transformBeforeSubmit: (formData) => {
// Transform args for an action that needs some arguments
if (formData?.ARGS) {
if (formData?.ARGS?.NAME) {
formData.ARGS = formData.ARGS.NAME
} else if (formData?.ARGS?.SNAPSHOT_ID) {
formData.ARGS = formData.ARGS.SNAPSHOT_ID
}
}
return formData
},
})
export default PerformActionForm

View File

@ -0,0 +1,132 @@
/* ------------------------------------------------------------------------- *
* 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. *
* ------------------------------------------------------------------------- */
import { ObjectSchema, string } from 'yup'
import { getObjectSchemaFromFields, arrayToOptions, Field } from 'client/utils'
import {
INPUT_TYPES,
T,
VM_ACTIONS_WITH_SCHEDULE,
VM_ACTIONS,
ARGS_TYPES,
} from 'client/constants'
import { getRequiredArgsByAction } from 'client/models/Scheduler'
/**
* @returns {Field} Action name field
*/
const ACTION_FIELD = {
name: 'ACTION',
label: T.Action,
type: INPUT_TYPES.AUTOCOMPLETE,
optionsOnly: true,
values: () => {
const validActions = {
...VM_ACTIONS_WITH_SCHEDULE,
}
/**
* BACKUP: Not supported by oneflow api
*/
delete validActions[VM_ACTIONS.BACKUP]
return arrayToOptions(
Object.entries({
...validActions,
}),
{
addEmpty: false,
getText: ([, text]) => text,
getValue: ([value]) => value,
}
)
},
validation: string().trim().required(),
grid: { xs: 12 },
}
export const ACTION_FIELD_NAME = 'ACTION'
const createArgField = (argName, htmlType) => ({
name: `ARGS.${argName}`,
dependOf: ACTION_FIELD_NAME,
htmlType: (action) => {
const prueba = getRequiredArgsByAction(action)
console.log(argName, prueba.includes(argName))
return !getRequiredArgsByAction(action)?.includes(argName)
? INPUT_TYPES.HIDDEN
: htmlType
},
})
/** @type {Field} Snapshot name field */
const ARGS_NAME_FIELD = {
...createArgField(ARGS_TYPES.NAME),
label: T.SnapshotName,
type: INPUT_TYPES.TEXT,
}
/**
* @returns {Field} Snapshot id field
*/
const ARGS_SNAPSHOT_ID_FIELD = {
...createArgField(ARGS_TYPES.SNAPSHOT_ID),
label: T.Snapshot + ' ' + T.ID,
type: INPUT_TYPES.TEXT,
}
const ROLE_FIELD = (roles) => ({
name: 'ROLE',
label: T.Role,
type: INPUT_TYPES.AUTOCOMPLETE,
optionsOnly: true,
values: () => {
const rolesWithAll = roles.map((role) => ({
name: role.name,
value: role.name,
}))
rolesWithAll.push({
name: T.All,
value: 'ALL',
})
return arrayToOptions(rolesWithAll, {
addEmpty: false,
getText: (role) => role.name,
getValue: (role) => role.value,
})
},
validation: string().trim().required(),
grid: { xs: 12 },
})
/**
* @param {object} props - Properties of the form
* @param {object} props.roles - Roles of the service
* @returns {Array} - List of fields
*/
export const FIELDS = ({ roles }) => [
ACTION_FIELD,
ROLE_FIELD(roles),
ARGS_NAME_FIELD,
ARGS_SNAPSHOT_ID_FIELD,
]
/** @type {ObjectSchema} Schema */
export const SCHEMA = ({ roles }) =>
getObjectSchemaFromFields(FIELDS({ roles }))

View File

@ -14,37 +14,14 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import { useGetServiceQuery } from 'client/features/OneApi/service'
// import ScheduleActionCard from 'client/components/Cards/ScheduleActionCard'
import { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC'
import { CreateStepsCallback } from 'client/utils/schema'
/**
* Renders the list of schedule actions from a Service.
*
* @param {object} props - Props
* @param {string} props.id - Service id
* @param {object|boolean} props.tabProps - Tab properties
* @param {object} [props.tabProps.actions] - Actions from user view yaml
* @returns {ReactElement} Schedule actions tab
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
*/
const SchedulingTab = ({ id, tabProps: { actions } = {} }) => {
const { data: service = {} } = useGetServiceQuery({ id })
const CreatePerformAction = (configProps) =>
AsyncLoadForm({ formPath: 'Service/PerformAction' }, configProps)
return (
<>
<Stack gap="1em" py="0.8em">
{service?.NAME}
{/* TODO: scheduler actions & form */}
</Stack>
</>
)
}
SchedulingTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
export default SchedulingTab
export { CreatePerformAction }

View File

@ -31,7 +31,6 @@ import { Tr } from 'client/components/HOC'
import { Legend } from 'client/components/Forms'
import { mapNameByIndex } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/schema'
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/ServiceTemplate/CreateForm/Steps/Extra'
import { Component, useMemo } from 'react'
@ -52,7 +51,7 @@ const ScheduleActionsSection = ({ oneConfig, adminGroup }) => {
update,
append,
} = useFieldArray({
name: `${EXTRA_ID}.${TAB_ID}`,
name: `charter.${TAB_ID}`,
keyName: 'ID',
})

View File

@ -48,14 +48,41 @@ const Steps = createSteps([General, UserInputs, Network, Charter], {
}
)
// Get schedule actions from vm template contents
const schedActions = parseVmTemplateContents(
ServiceTemplate?.TEMPLATE?.BODY?.roles[0]?.vm_template_contents,
true
)?.schedActions
const knownTemplate = schema.cast({
[GENERAL_ID]: {},
[USERINPUTS_ID]: {},
[NETWORK_ID]: { NETWORKS: networks },
[CHARTER_ID]: {},
[CHARTER_ID]: { SCHED_ACTION: schedActions },
})
return { ...knownTemplate, roles: roles }
const newRoles = roles.map((role) => {
// Parse vm template content
const roleTemplateContent = parseVmTemplateContents(
role.vm_template_contents,
true
)
// Delete schedule actions
delete roleTemplateContent.schedActions
// Parse content without sched actions
const roleTemplateWithoutSchedActions = parseVmTemplateContents(
roleTemplateContent,
false
)
role.vm_template_contents = roleTemplateWithoutSchedActions
// Return content
return role
})
return { ...knownTemplate, roles: newRoles }
},
transformBeforeSubmit: (formData) => {
@ -86,12 +113,12 @@ const Steps = createSteps([General, UserInputs, Network, Charter], {
{
vmTemplateContents: role?.vm_template_contents,
customAttrsValues: userInputsData,
schedActions: charterData.SCHED_ACTION,
},
false,
true
),
})),
...(!!charterData?.SCHED_ACTION?.length && { ...charterData }),
name: generalData?.NAME,
}

View File

@ -0,0 +1,148 @@
/* ------------------------------------------------------------------------- *
* 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. *
* ------------------------------------------------------------------------- */
import { ReactElement, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import { parseVmTemplateContents } from 'client/utils'
import {
useGetServiceQuery,
useServiceRoleActionMutation,
useServiceAddActionMutation,
} from 'client/features/OneApi/service'
import {} from 'client/features/OneApi/vm'
import ScheduleActionCard from 'client/components/Cards/ScheduleActionCard'
import { PerformActionButton } from 'client/components/Buttons/ScheduleAction'
import { getScheduleActions } from 'client/models/VirtualMachine'
import { VM_ACTIONS, T } from 'client/constants'
import { useGeneralApi } from 'client/features/General'
const { PERFORM_ACTION } = VM_ACTIONS
/**
* Renders the list of schedule actions from a Service.
*
* @param {object} props - Props
* @param {string} props.id - Service id
* @param {object|boolean} props.tabProps - Tab properties
* @param {object} [props.tabProps.actions] - Actions from user view yaml
* @returns {ReactElement} Schedule actions tab
*/
const SchedulingTab = ({ id, tabProps: { actions } = {} }) => {
const { enqueueError, enqueueSuccess, enqueueInfo } = useGeneralApi()
// Get service info
const { data: service = {} } = useGetServiceQuery({ id })
// Functions to manage sched actions
const [useServiceAddAction] = useServiceAddActionMutation()
const [serviceRoleAction] = useServiceRoleActionMutation()
// Check actions and roles
const [scheduling, actionsAvailable, roles] = useMemo(() => {
const schedActions = {
TEMPLATE: {
SCHED_ACTION: parseVmTemplateContents(
service?.TEMPLATE?.BODY?.roles[0]?.vm_template_contents,
true
)?.schedActions,
},
}
const updatedRoles = service?.TEMPLATE?.BODY?.roles
return [getScheduleActions(schedActions), actions, updatedRoles]
}, [service])
const isPerformActionEnabled = actionsAvailable[PERFORM_ACTION]
/**
* Add new schedule action to VM.
*
* @param {object} formData - New schedule action
* @returns {Promise} - Add schedule action and refetch VM data
*/
const handlePerformAction = async (formData) => {
enqueueInfo(T.InfoServiceActionRole, [formData.ACTION, formData.ROLE])
try {
if (formData.ROLE === 'ALL') {
await useServiceAddAction({
id,
perform: formData.ACTION,
params: {
args: formData.ARGS,
},
})
enqueueSuccess(T.SuccessRoleActionCompleted, [
formData.ACTION,
formData.ROLE,
])
} else {
await serviceRoleAction({
id,
role: formData.ROLE,
perform: formData.ACTION,
params: {
args: formData.ARGS,
},
})
enqueueSuccess(T.SuccessRoleActionCompleted, [
formData.ACTION,
formData.ROLE,
])
}
} catch (error) {
enqueueError(T.ErrorServiceActionRole, [
formData?.ACTION,
formData?.ROLE,
error,
])
}
}
return (
<>
{isPerformActionEnabled && (
<Stack flexDirection="row" gap="1em">
{isPerformActionEnabled && (
<PerformActionButton onSubmit={handlePerformAction} roles={roles} />
)}
</Stack>
)}
<Stack gap="1em" py="0.8em">
{scheduling.map((schedule) => {
const { ID, NAME } = schedule
return <ScheduleActionCard key={ID ?? NAME} schedule={schedule} />
})}
</Stack>
</>
)
}
SchedulingTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
export default SchedulingTab

View File

@ -23,7 +23,7 @@ import { useGetServiceQuery } from 'client/features/OneApi/service'
import { getAvailableInfoTabs } from 'client/models/Helper'
import Tabs from 'client/components/Tabs'
import Actions from 'client/components/Tabs/Service/Actions'
import Actions from 'client/components/Tabs/Service/SchedActions'
import Info from 'client/components/Tabs/Service/Info'
import Log from 'client/components/Tabs/Service/Log'
import Roles from 'client/components/Tabs/Service/Roles'
@ -33,7 +33,7 @@ const getTabComponent = (tabName) =>
info: Info,
roles: Roles,
log: Log,
schedulerAction: Actions,
sched_actions: Actions,
}[tabName])
const ServiceTabs = memo(({ id }) => {

View File

@ -151,6 +151,8 @@ module.exports = {
Migrate: 'Migrate',
MigrateLive: 'Migrate live',
Offline: 'Offline',
PerformAction: 'Perform action',
AllRoles: 'All roles',
Pin: 'Pin',
Poweroff: 'Poweroff',
PoweroffHard: 'Poweroff hard',
@ -1515,6 +1517,8 @@ module.exports = {
RoleManageApps: 'Manage multi-VM applications efficiently.',
/* Service Template - configuration */
RoleConfiguration: 'Role Configuration',
/* Service Template - schedule actions */
ServiceSheduleActionCreated: 'Shedule action added to service',
/* VMGroups - Role definition */
NewRole: 'New Role',

View File

@ -804,6 +804,7 @@ export const VM_ACTIONS = {
SCHED_ACTION_UPDATE: 'sched-update',
SCHED_ACTION_DELETE: 'sched-delete',
CHARTER_CREATE: 'charter_create',
PERFORM_ACTION: 'perform_action',
// CONFIGURATION
UPDATE_CONF: 'update_configuration',

View File

@ -102,7 +102,7 @@ const extractPropertiesToArray = (content) => {
}
const formatInstantiate = (contents) => {
const { vmTemplateContents, customAttrsValues } = contents
const { vmTemplateContents, customAttrsValues, schedActions } = contents
const sections = extractSections(vmTemplateContents)
.map(parseSection)
@ -139,7 +139,12 @@ const formatInstantiate = (contents) => {
...filteredProperties,
]
const formattedTemplate = combinedContent.join('\n') + '\n'
const formattedActions = schedActions?.map((action, index) =>
formatSchedActions({ ...action, ID: index })
)
const formattedTemplate =
combinedContent.join('\n') + formattedActions.join('\n') + '\n'
return formattedTemplate
}