import * as React from 'react'; import * as yaml from 'js-yaml'; import { Alert, AlertActionCloseButton, AlertVariant, Button, ButtonVariant, Form, FormGroup, Grid, GridItem, ModalBoxBody, ModalBoxFooter, Text, TextInputTypes, TextVariants, } from '@patternfly/react-core'; import { Formik, FormikProps, FormikConfig, FieldArray, useField, useFormikContext } from 'formik'; import * as Yup from 'yup'; import { InfraEnvK8sResource, SecretK8sResource } from '../../types'; import { InputField, macAddressValidationSchema, CodeField, getFieldId, richNameValidationSchema, getRichTextValidation, RichInputField, HOSTNAME_VALIDATION_MESSAGES, bmcAddressValidationSchema, } from '../../../common'; import { Language } from '@patternfly/react-code-editor'; import { MinusCircleIcon, PlusCircleIcon } from '@patternfly/react-icons'; import { AddBmcValues, BMCFormProps } from './types'; import { NMStateK8sResource } from '../../types/k8s/nm-state'; import { BareMetalHostK8sResource } from '../../types/k8s/bare-metal-host'; import { AGENT_BMH_NAME_LABEL_KEY, BMH_HOSTNAME_ANNOTATION, INFRAENV_AGENTINSTALL_LABEL_KEY, } from '../common'; const MacMapping = () => { const [field, { touched, error }] = useField<{ macAddress: string; name: string }[]>({ name: 'macMapping', }); const { errors } = useFormikContext(); const fieldId = getFieldId('macMapping', 'input'); const isValid = !(touched && error); return ( <FormGroup fieldId={fieldId} label="Mac to interface name mapping" validated={isValid ? 'default' : 'error'} > <FieldArray name="macMapping" render={({ push, remove }) => ( <Grid hasGutter> <GridItem span={5}> <Text component={TextVariants.small}>MAC address</Text> </GridItem> <GridItem span={5}> <Text component={TextVariants.small}>NIC</Text> </GridItem> {field.value.map((mac, index) => { const macField = `macMapping[${index}].macAddress`; const nameField = `macMapping[${index}].name`; return ( <React.Fragment key={index}> <GridItem span={5}> <InputField name={macField} inputError={errors[macField]?.[0]} /> </GridItem> <GridItem span={5}> <InputField name={nameField} inputError={errors[nameField]?.[0]} /> </GridItem> {index !== 0 && ( <GridItem span={2}> <MinusCircleIcon onClick={() => remove(index)} /> </GridItem> )} </React.Fragment> ); })} <GridItem span={6}> <Button icon={<PlusCircleIcon />} onClick={() => push({ macAddress: '', name: '' })} variant="link" isInline > Add more </Button> </GridItem> </Grid> )} /> </FormGroup> ); }; const getNMState = (values: AddBmcValues, infraEnv: InfraEnvK8sResource): NMStateK8sResource => { // eslint-disable-next-line const config: any = yaml.load(values.nmState); const nmState = { apiVersion: 'agent-install.openshift.io/v1beta1', kind: 'NMStateConfig', metadata: { generateName: `${infraEnv.metadata?.name}-`, namespace: infraEnv.metadata?.namespace, labels: { [AGENT_BMH_NAME_LABEL_KEY]: values.name, [INFRAENV_AGENTINSTALL_LABEL_KEY]: infraEnv?.metadata?.name || '', }, }, spec: { config, interfaces: values.macMapping.filter((m) => m.macAddress.length && m.name.length), }, }; return nmState; }; const getValidationSchema = (usedHostnames: string[], origHostname: string) => Yup.object({ name: Yup.string().required(), hostname: richNameValidationSchema(usedHostnames, origHostname), bmcAddress: bmcAddressValidationSchema.required(), username: Yup.string().required(), password: Yup.string().required(), bootMACAddress: macAddressValidationSchema, nmState: Yup.string(), macMapping: Yup.array().of( Yup.object().shape( { macAddress: macAddressValidationSchema.when(['name'], { is: (name) => !!name, then: macAddressValidationSchema.required('MAC has to be specified'), }), name: Yup.string().when(['macAddress'], { is: (macAddress) => !!macAddress, then: Yup.string().required('Name has to be specified'), }), }, [['name', 'macAddress']], ), ), }); const emptyValues: AddBmcValues = { name: '', hostname: '', bmcAddress: '', username: '', password: '', bootMACAddress: '', disableCertificateVerification: true, // TODO(mlibra) online: false, // TODO(mlibra) nmState: '', macMapping: [{ macAddress: '', name: '' }], }; const getInitValues = ( bmh?: BareMetalHostK8sResource, nmState?: NMStateK8sResource, secret?: SecretK8sResource, isEdit?: boolean, ): AddBmcValues => { if (isEdit) { return { name: bmh?.metadata?.name || '', hostname: bmh?.metadata?.annotations?.[BMH_HOSTNAME_ANNOTATION] || '', bmcAddress: bmh?.spec?.bmc?.address || '', username: secret?.data?.username ? atob(secret.data.username) : '', password: secret?.data?.password ? atob(secret.data.password) : '', bootMACAddress: bmh?.spec?.bootMACAddress || '', disableCertificateVerification: !!bmh?.spec?.bmc?.disableCertificateVerification, online: !!bmh?.spec?.online, nmState: nmState ? yaml.dump(nmState?.spec?.config) : '', macMapping: nmState?.spec?.interfaces || [{ macAddress: '', name: '' }], }; } else { return emptyValues; } }; const BMCForm: React.FC<BMCFormProps> = ({ onCreateBMH, onClose, hasDHCP, infraEnv, bmh, nmState, secret, isEdit, usedHostnames, }) => { const [error, setError] = React.useState(); const handleSubmit: FormikConfig<AddBmcValues>['onSubmit'] = async (values) => { try { setError(undefined); const nmState = values.nmState ? getNMState(values, infraEnv) : undefined; await onCreateBMH(values, nmState); onClose(); } catch (e) { setError(e.message); } }; const { initValues, validationSchema } = React.useMemo(() => { const initValues = getInitValues(bmh, nmState, secret, isEdit); const validationSchema = getValidationSchema(usedHostnames, initValues.hostname); return { initValues, validationSchema }; }, [usedHostnames, bmh, nmState, secret, isEdit]); return ( <Formik initialValues={initValues} isInitialValid={false} validate={getRichTextValidation(validationSchema)} onSubmit={handleSubmit} > {({ isSubmitting, isValid, submitForm }: FormikProps<AddBmcValues>) => ( <> <ModalBoxBody> <Form id="add-bmc-form"> <InputField label="Name" name="name" placeholder="Enter the name for the Host" isRequired isDisabled={isEdit} /> <RichInputField label="Hostname" name="hostname" placeholder="Enter the hostname for the Host" richValidationMessages={HOSTNAME_VALIDATION_MESSAGES} isRequired /> <InputField label="Baseboard Management Controller Address" name="bmcAddress" placeholder="Enter an address" isRequired /> <InputField label="Boot NIC MAC Address" name="bootMACAddress" placeholder="Enter an address" description="The MAC address of the host's network connected NIC that wll be used to provision the host." /> <InputField label="Username" name="username" placeholder="Enter a username for the BMC" isRequired /> <InputField type={TextInputTypes.password} label="Password" name="password" placeholder="Enter a password for the BMC" isRequired /> {!hasDHCP && ( <> <CodeField label="NMState" name="nmState" language={Language.yaml} description="Upload a YAML file in NMstate format that includes your network configuration (static IPs, bonds, etc.)." /> <MacMapping /> </> )} </Form> {error && ( <Alert title="Failed to add host" variant={AlertVariant.danger} isInline actionClose={<AlertActionCloseButton onClose={() => setError(undefined)} />} > {error} </Alert> )} </ModalBoxBody> <ModalBoxFooter> <Button onClick={submitForm} isDisabled={isSubmitting || !isValid}> {isEdit ? 'Submit' : 'Create'} </Button> <Button onClick={onClose} variant={ButtonVariant.secondary}> Cancel </Button> </ModalBoxFooter> </> )} </Formik> ); }; export default BMCForm;