// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance // with the License. A copy of the License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and // limitations under the License. // Fameworks import * as React from 'react' import i18next from 'i18next' import {Trans, useTranslation} from 'react-i18next' import {findFirst, clamp} from '../../util' // UI Elements import { Button, ColumnLayout, Container, FormField, Header, Input, InputProps, Select, SpaceBetween, TextContent, Checkbox, } from '@cloudscape-design/components' // State import {getState, setState, useState, clearState} from '../../store' // Components import {LabeledIcon} from './Components' import { Storages, StorageType, STORAGE_TYPE_PROPS, UIStorageSettings, } from './Storage.types' import {useFeatureFlag} from '../../feature-flags/useFeatureFlag' import InfoLink from '../../components/InfoLink' import TitleDescriptionHelpPanel from '../../components/help-panel/TitleDescriptionHelpPanel' import {useMemo} from 'react' import {useHelpPanel} from '../../components/help-panel/HelpPanel' // Constants const storagePath = ['app', 'wizard', 'config', 'SharedStorage'] const errorsPath = ['app', 'wizard', 'errors', 'sharedStorage'] // Types type StorageTypeOption = [string, string, string] // Helper Functions function itemToIconOption([value, label, icon]: StorageTypeOption) { return {value: value, label: label, iconUrl: icon} } function itemToDisplayIconOption([value, label, icon]: StorageTypeOption) { return { value: value, label: icon ? : label, } } function strToOption(str: any) { return {value: str, label: str.toString()} } function storageValidate() { const storageSettings = getState(storagePath) let valid = true if (storageSettings) for (let i = 0; i < storageSettings.length; i++) { const settingsType = getState([...storagePath, i, 'StorageType']) if (settingsType === 'Ebs') { const volumeId = getState([ ...storagePath, i, 'EbsSettings', 'VolumeId', ]) const volumeSize = getState([...storagePath, i, 'EbsSettings', 'Size']) if ( !volumeId && (volumeSize === null || volumeSize === '' || volumeSize < 35 || volumeSize > 2048) ) { setState( [...errorsPath, i, 'EbsSettings', 'Size'], i18next.t('wizard.storage.validation.volumeSize'), ) valid = false } else { clearState([...errorsPath, i, 'EbsSettings', 'Size']) } } } setState([...errorsPath, 'validated'], true) const config = getState(['app', 'wizard', 'config']) console.log(config) return valid } const LUSTRE_PERSISTENT1_DEFAULT_THROUGHPUT = 200 const LUSTRE_PERSISTENT2_DEFAULT_THROUGHPUT = 125 const storageThroughputsP1 = [50, 100, LUSTRE_PERSISTENT1_DEFAULT_THROUGHPUT] const storageThroughputsP2 = [ LUSTRE_PERSISTENT2_DEFAULT_THROUGHPUT, 250, 500, 1000, ] export function FsxLustreSettings({index}: any) { const {t} = useTranslation() const isLustrePersistent2Active = useFeatureFlag('lustre_persistent2') const useExisting = useState(['app', 'wizard', 'storage', 'ui', index, 'useExisting']) || false const fsxPath = [...storagePath, index, 'FsxLustreSettings'] const storageCapacityPath = [...fsxPath, 'StorageCapacity'] const lustreTypePath = [...fsxPath, 'DeploymentType'] // support FSx Lustre PERSISTENT_2 only in >= 3.2.0 const lustreTypes = [ isLustrePersistent2Active ? 'PERSISTENT_2' : null, 'PERSISTENT_1', 'SCRATCH_1', 'SCRATCH_2', ].filter(Boolean) const storageThroughputPath = [...fsxPath, 'PerUnitStorageThroughput'] const importPathPath = [...fsxPath, 'ImportPath'] const exportPathPath = [...fsxPath, 'ExportPath'] const compressionPath = [...fsxPath, 'DataCompressionType'] const storageCapacity = useState(storageCapacityPath) const lustreType = useState(lustreTypePath) const storageThroughput = useState(storageThroughputPath) const importPath = useState(importPathPath) || '' const exportPath = useState(exportPathPath) || '' const compression = useState(compressionPath) React.useEffect(() => { const fsxPath = [...storagePath, index, 'FsxLustreSettings'] const storageCapacityPath = [...fsxPath, 'StorageCapacity'] const lustreTypePath = [...fsxPath, 'DeploymentType'] if (storageCapacity === null && !useExisting) setState(storageCapacityPath, 1200) if (!storageThroughput && !useExisting) { setState( storageThroughputPath, lustreType === 'PERSISTENT_1' ? LUSTRE_PERSISTENT1_DEFAULT_THROUGHPUT : LUSTRE_PERSISTENT2_DEFAULT_THROUGHPUT, ) } if (lustreType === null && !useExisting) setState( lustreTypePath, isLustrePersistent2Active ? 'PERSISTENT_2' : 'PERSISTENT_1', ) // eslint-disable-next-line react-hooks/exhaustive-deps }, [ storageCapacity, lustreType, storageThroughput, index, useExisting, isLustrePersistent2Active, ]) const toggleCompression = () => { if (compression) clearState(compressionPath) else setState(compressionPath, 'LZ4') } const setImportPath = (path: any) => { if (path !== '') setState(importPathPath, path) else clearState(importPathPath) } const setExportPath = (path: any) => { if (path !== '') setState(exportPathPath, path) else clearState(exportPathPath) } const capacityMin = 1200 const capacityMax = 100800 const capacityStep = 1200 const clampCapacity = (inCapacityStr: string) => { return clamp( parseInt(inCapacityStr), capacityMin, capacityMax, capacityStep, ).toString() } return (
{ setState(storageCapacityPath, detail.value) }} onBlur={_e => { setState(storageCapacityPath, clampCapacity(storageCapacity)) }} type="number" />
} info={ } /> } /> } > setImportPath(detail.value)} /> } /> } /> } > { setExportPath(detail.value) }} /> )} {['PERSISTENT_1', 'PERSISTENT_2'].includes(lustreType) && ( } /> } /> } > { setState(kmsPath, detail.value) }} /> ) : null}
{ console.log('value: ', detail.value, parseInt(detail.value)) setState( provisionedThroughputPath, clamp(parseInt(detail.value), 1, 1024).toString(), ) }} />
)}
) } function EbsSettings({index}: any) { const {t} = useTranslation() const ebsPath = [...storagePath, index, 'EbsSettings'] const volumeTypePath = [...ebsPath, 'VolumeType'] const volumeTypes = ['gp3', 'gp2', 'io1', 'io2', 'sc1', 'st1', 'standard'] const defaultVolumeType = 'gp3' const volumeSizePath = [...ebsPath, 'Size'] const encryptedPath = [...ebsPath, 'Encrypted'] const kmsPath = [...ebsPath, 'KmsKeyId'] const snapshotIdPath = [...ebsPath, 'SnapshotId'] const deletionPolicyPath = [...ebsPath, 'DeletionPolicy'] const deletionPolicies = ['Delete', 'Retain', 'Snapshot'] const volumeErrors = useState([...errorsPath, index, 'EbsSettings', 'Size']) let volumeType = useState(volumeTypePath) let volumeSize = useState(volumeSizePath) let encrypted = useState(encryptedPath) let kmsId = useState(kmsPath) let snapshotId = useState(snapshotIdPath) let deletionPolicy = useState(deletionPolicyPath) let validated = useState([...errorsPath, 'validated']) React.useEffect(() => { const ebsPath = [...storagePath, index, 'EbsSettings'] const volumeTypePath = [...ebsPath, 'VolumeType'] const deletionPolicyPath = [...ebsPath, 'DeletionPolicy'] const volumeSizePath = [...ebsPath, 'Size'] if (volumeType === null) setState(volumeTypePath, defaultVolumeType) if (deletionPolicy === null) setState(deletionPolicyPath, 'Delete') if (volumeSize === null) setState(volumeSizePath, 35) }, [volumeType, volumeSize, deletionPolicy, index]) const toggleEncrypted = () => { const setEncrypted = !encrypted setState(encryptedPath, setEncrypted) if (!setEncrypted) clearState(kmsPath) } return (
: { setState(volumeSizePath, detail.value) validated && storageValidate() }} />
} /> } /> } > {t('wizard.storage.Ebs.encrypted.label')} {encrypted ? ( { setState(kmsPath, detail.value) }} /> ) : null} } /> } /> } > { setState(snapshotIdPath, snapshotId === null ? '' : null) }} > {t('wizard.storage.Ebs.snapshotId.label')} {snapshotId !== null && ( { setState(snapshotIdPath, detail.value) }} /> )} } /> } /> } > { setState([...storagePath, index, 'MountDir'], detail.value) }} />
{STORAGE_TYPE_PROPS[storageType].maxToCreate > 0 ? ( } /> } /> ) : null} {useExisting && { Ebs: (
{t('wizard.storage.Ebs.existing')}: { setState(existingPath, detail.value) }} />
), FsxLustre: ( { setState(existingPath, detail.selectedOption.value) }} options={fsxVolumes.zfs.map((vol: any) => ({ value: vol.id, label: vol.displayName, }))} /> ), FsxOntap: ( { setState(existingPath, detail.selectedOption.value) }} options={efsFilesystems.map((x: any) => { return { value: x.FileSystemId, label: x.FileSystemId + (x.Name ? ` (${x.Name})` : ''), } })} /> ), }[storageType]}
{!useExisting && { FsxLustre: , Efs: , Ebs: , FsxOntap: null, FsxOpenZfs: null, }[storageType]} ) } function Storage() { const {t} = useTranslation() const storages = useState(storagePath) const uiStorageSettings = useState(['app', 'wizard', 'storage', 'ui']) const storageType = useState(['app', 'wizard', 'storage', 'type']) const isFsxOnTapActive = useFeatureFlag('fsx_ontap') const isFsxOpenZsfActive = useFeatureFlag('fsx_openzsf') const canEditFilesystems = useDynamicStorage() useHelpPanel() const storageMaxes: Record = { FsxLustre: 21, FsxOntap: 20, FsxOpenZfs: 20, Efs: 21, Ebs: 5, } /* Activate ONTAP/OpenZFS only from ParallelCluster 3.2.0 */ const storageTypesSource: StorageTypeOption[] = [ ['FsxLustre', 'Amazon FSx for Lustre (FSX)', '/img/fsx.svg'], isFsxOnTapActive ? ['FsxOntap', 'Amazon FSx for NetApp ONTAP (FSX)', '/img/fsx.svg'] : false, isFsxOpenZsfActive ? ['FsxOpenZfs', 'Amazon FSx for OpenZFS (FSX)', '/img/fsx.svg'] : false, ['Efs', 'Amazon Elastic File System (EFS)', '/img/efs.svg'], ['Ebs', 'Amazon Elastic Block Store (EBS)', '/img/ebs.svg'], ].filter(Boolean) as StorageTypeOption[] const defaultCounts = {FsxLustre: 0, Efs: 0, Ebs: 0} const storageReducer = (eax: any, item: any) => { let ret = {...eax} ret[item.StorageType] += 1 return ret } const storageCounts = storages ? storages.reduce(storageReducer, defaultCounts) : defaultCounts const storageTypes = storageTypesSource.reduce( (newStorages: StorageTypeOption[], storageType: StorageTypeOption) => { const st = storageType[0] return storageCounts[st] >= storageMaxes[st] ? newStorages : [...newStorages, storageType] }, [], ) const addStorage = () => { const newIndex = storages ? storages.length : 0 const uiSettingsPath = ['app', 'wizard', 'storage', 'ui'] const useExisting = !canCreateStorage( storageType, storages, uiStorageSettings, ) if (!storages) { setState(storagePath, [ { Name: `${storageType}${newIndex}`, StorageType: storageType, MountDir: '/shared', }, ]) setState(uiSettingsPath, [{useExisting}]) } else { setState([...storagePath, newIndex], { Name: `${storageType}${newIndex}`, StorageType: storageType, MountDir: '/shared', }) setState([...uiSettingsPath, newIndex], {useExisting}) } clearState(['app', 'wizard', 'storage', 'type']) } const setStorageType = (newStorageType: any) => { setState(['app', 'wizard', 'storage', 'type'], newStorageType) } return ( {t('wizard.storage.container.title')}
{storages ? ( storages.map((_: any, i: any) => ( )) ) : ( {t('wizard.storage.container.noStorageSelected')} )} {canEditFilesystems && storageTypes.length > 0 && (
{t('wizard.storage.container.storageType')}