Skip to content

Commit

Permalink
CNV-36070: Storageclass migration select volumes
Browse files Browse the repository at this point in the history
  • Loading branch information
upalatucci committed Nov 5, 2024
1 parent 8647e80 commit 8366043
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 8 deletions.
4 changes: 4 additions & 0 deletions locales/en/plugin__kubevirt-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@
"Migrate VirtualMachine storage to a different StorageClass.": "Migrate VirtualMachine storage to a different StorageClass.",
"Migration": "Migration",
"Migration chart": "Migration chart",
"Migration details": "Migration details",
"Migration metrics": "Migration metrics",
"Migration Toolkit for Virtualization": "Migration Toolkit for Virtualization",
"MigrationPolicies": "MigrationPolicies",
Expand Down Expand Up @@ -1077,12 +1078,14 @@
"Select StorageClass": "Select StorageClass",
"Select target node": "Select target node",
"Select the destination storage for the VirtualMachine storage migration.": "Select the destination storage for the VirtualMachine storage migration.",
"Select the storage to migrate for": "Select the storage to migrate for",
"Select this option if you use an on-premise subscription service": "Select this option if you use an on-premise subscription service",
"Select volume to boot from": "Select volume to boot from",
"Select VolumeSnapshot": "Select VolumeSnapshot",
"Select workloads that must have all the following expressions.": "Select workloads that must have all the following expressions.",
"Selected columns will appear in the table.": "Selected columns will appear in the table.",
"Selected StorageClass is different from StorageClass of the source": "Selected StorageClass is different from StorageClass of the source",
"Selected volumes": "Selected volumes",
"Selector": "Selector",
"selector key": "selector key",
"selector value": "selector value",
Expand Down Expand Up @@ -1226,6 +1229,7 @@
"The Descheduler can be used to evict a running VirtualMachine so that the VirtualMachine can be rescheduled onto a more suitable Node via a live migration.": "The Descheduler can be used to evict a running VirtualMachine so that the VirtualMachine can be rescheduled onto a more suitable Node via a live migration.",
"The disk must be attached to the VirtualMAchine as a SCSI LUN for this option to work. It should only be used for cluster-aware applications": "The disk must be attached to the VirtualMAchine as a SCSI LUN for this option to work. It should only be used for cluster-aware applications",
"The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.",
"The entire VirtualMachine": "The entire VirtualMachine",
"The following areas have pending changes that will be applied when this VirtualMachine is restarted.": "The following areas have pending changes that will be applied when this VirtualMachine is restarted.",
"The following credentials for this operating system were created via cloud-init. If unsuccessful, cloud-init could be improperly configured. Contact the image provider for more information.": "The following credentials for this operating system were created via cloud-init. If unsuccessful, cloud-init could be improperly configured. Contact the image provider for more information.",
"The following disk will not be included in the snapshot_one": "The following disk will not be included in the snapshot",
Expand Down
25 changes: 23 additions & 2 deletions src/views/virtualmachines/migrate/VirtualMachineMigration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { FC, useCallback, useMemo, useState } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom-v5-compat';
import { VirtualMachineModel } from 'src/views/dashboard-extensions/utils';

import { IoK8sApiCoreV1PersistentVolumeClaim } from '@kubevirt-ui/kubevirt-api/kubernetes';
import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt';
import Loading from '@kubevirt-utils/components/Loading/Loading';
import useDefaultStorageClass from '@kubevirt-utils/hooks/useDefaultStorage/useDefaultStorageClass';
Expand All @@ -21,8 +22,9 @@ import {
} from '@patternfly/react-core';

import VirtualMachineMigrationDestinationTab from './tabs/VirtualMachineMigrationDestinationTab';
import VirtualMachineMigrationDetails from './tabs/VirtualMachineMigrationDetails';
import VirtualMachineMigrationReviewTab from './tabs/VirtualMachineMigrationReviewTab';
import { migrateVM } from './utils';
import { entireVMSelected, migrateVM } from './utils';

const VirtualMachineMigrate: FC = () => {
const { t } = useKubevirtTranslation();
Expand All @@ -31,6 +33,9 @@ const VirtualMachineMigrate: FC = () => {
const [selectedStorageClass, setSelectedStorageClass] = useState('');
const [migrationError, setMigrationError] = useState<Error | null>(null);
const [migrationLoading, setMigrationLoading] = useState(false);
const [selectedPVCs, setSelectedPVCs] = useState<IoK8sApiCoreV1PersistentVolumeClaim[] | null>(
null,
);

const navigate = useNavigate();
const [searchParams] = useSearchParams();
Expand Down Expand Up @@ -73,7 +78,11 @@ const VirtualMachineMigrate: FC = () => {
setMigrationLoading(true);
setMigrationError(null);
try {
await migrateVM(vm, pvcs, destinationStorageClass);
await migrateVM(
vm,
entireVMSelected(selectedPVCs) ? pvcs : selectedPVCs,
destinationStorageClass,
);

goBack();
} catch (apiError) {
Expand Down Expand Up @@ -110,6 +119,18 @@ const VirtualMachineMigrate: FC = () => {
onSave={onSubmit}
title={t('Migrate VirtualMachine storage')}
>
<WizardStep
footer={{ isNextDisabled: selectedPVCs?.length === 0 }}
id="wizard-migration-details"
name={t('Migration details')}
>
<VirtualMachineMigrationDetails
pvcs={pvcs}
selectedPVCs={selectedPVCs}
setSelectedPVCs={setSelectedPVCs}
vm={vm}
/>
</WizardStep>
<WizardStep id="wizard-migrate-destination" name={t('Destination StorageClass')}>
<VirtualMachineMigrationDestinationTab
defaultStorageClassName={defaultStorageClassName}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { Dispatch, FC, SetStateAction } from 'react';

import { IoK8sApiCoreV1PersistentVolumeClaim } from '@kubevirt-ui/kubevirt-api/kubernetes';
import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt';
import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import { VirtualMachineModelGroupVersionKind } from '@kubevirt-utils/models';
import { getName, getNamespace } from '@kubevirt-utils/resources/shared';
import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk';
import { Radio, Stack, StackItem, Text, TextVariants, Title } from '@patternfly/react-core';

import { entireVMSelected } from '../utils';

import SelectMigrationDisksTable from './components/SelectMigrationDisksTable';

type VirtualMachineMigrationDetailsProps = {
pvcs: IoK8sApiCoreV1PersistentVolumeClaim[];
selectedPVCs: IoK8sApiCoreV1PersistentVolumeClaim[];
setSelectedPVCs: Dispatch<SetStateAction<IoK8sApiCoreV1PersistentVolumeClaim[]>>;
vm: V1VirtualMachine;
};

const VirtualMachineMigrationDetails: FC<VirtualMachineMigrationDetailsProps> = ({
pvcs,
selectedPVCs,
setSelectedPVCs,
vm,
}) => {
const { t } = useKubevirtTranslation();

const allVolumes = entireVMSelected(selectedPVCs);

return (
<Stack hasGutter>
<StackItem>
<Title headingLevel="h2">{t('Migration details')}</Title>
<Text component={TextVariants.p}>
{t('Select the storage to migrate for')}
<ResourceLink
groupVersionKind={VirtualMachineModelGroupVersionKind}
inline
name={getName(vm)}
namespace={getNamespace(vm)}
/>
</Text>
</StackItem>
<StackItem>
<Radio
id="all-volumes"
isChecked={allVolumes}
label={t('The entire VirtualMachine')}
name="volumes"
onChange={() => setSelectedPVCs(null)}
/>
<Radio
id="selected-volumes"
isChecked={!allVolumes}
label={t('Selected volumes')}
name="volumes"
onChange={() => setSelectedPVCs(pvcs)}
/>
</StackItem>
{!allVolumes && (
<StackItem>
<SelectMigrationDisksTable
pvcs={pvcs}
selectedPVCs={selectedPVCs}
setSelectedPVCs={setSelectedPVCs}
vm={vm}
/>
</StackItem>
)}
</Stack>
);
};

export default VirtualMachineMigrationDetails;
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, { Dispatch, FC, SetStateAction } from 'react';

import { IoK8sApiCoreV1PersistentVolumeClaim } from '@kubevirt-ui/kubevirt-api/kubernetes';
import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt';
import { getName } from '@kubevirt-utils/resources/shared';
import { getVolumes } from '@kubevirt-utils/resources/vm';
import { readableSizeUnit } from '@kubevirt-utils/utils/units';
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';

import { columnNames } from './constants';
import { getTableDiskData } from './utils';

type SelectMigrationDisksTableProps = {
pvcs: IoK8sApiCoreV1PersistentVolumeClaim[];
selectedPVCs: IoK8sApiCoreV1PersistentVolumeClaim[];
setSelectedPVCs: Dispatch<SetStateAction<IoK8sApiCoreV1PersistentVolumeClaim[]>>;
vm: V1VirtualMachine;
};

const SelectMigrationDisksTable: FC<SelectMigrationDisksTableProps> = ({
pvcs,
selectedPVCs,
setSelectedPVCs,
vm,
}) => {
const tableData = getVolumes(vm)?.map((volume) => getTableDiskData(vm, volume, pvcs));

const selectableData = tableData.filter((data) => data.isSelectable);

return (
<Table aria-label="Selectable table">
<Thead>
<Tr>
<Th
select={{
isSelected: selectedPVCs?.length === selectableData.length,
onSelect: (_event, isSelecting) =>
setSelectedPVCs(isSelecting ? selectableData.map((data) => data.pvc) : []),
}}
aria-label="Row select"
/>
<Th>{columnNames.name}</Th>
<Th>{columnNames.drive}</Th>
<Th>{columnNames.storageClass}</Th>
<Th>{columnNames.size}</Th>
</Tr>
</Thead>
<Tbody>
{tableData.map((diskData, rowIndex) => (
<Tr key={diskData.name}>
<Td
select={{
isDisabled: !diskData.isSelectable,
isSelected: Boolean(
selectedPVCs.find((pvc) => getName(pvc) === getName(diskData.pvc)),
),
onSelect: (_event, isSelecting) =>
setSelectedPVCs((selection) =>
isSelecting
? [...selection, diskData.pvc]
: selection.filter((pvc) => getName(pvc) !== getName(diskData.pvc)),
),
rowIndex,
}}
/>
<Td dataLabel={columnNames.name}>{diskData.name}</Td>
<Td dataLabel={columnNames.drive}>{diskData.drive}</Td>
<Td dataLabel={columnNames.storageClass}>{diskData.storageClass}</Td>
<Td dataLabel={columnNames.size}>{readableSizeUnit(diskData?.size)}</Td>
</Tr>
))}
</Tbody>
</Table>
);
};

export default SelectMigrationDisksTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const columnNames = {
drive: 'Drive',
name: 'Name',
size: 'Size',
storageClass: 'Storage class',
};
34 changes: 34 additions & 0 deletions src/views/virtualmachines/migrate/tabs/components/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { IoK8sApiCoreV1PersistentVolumeClaim } from '@kubevirt-ui/kubevirt-api/kubernetes';
import { V1VirtualMachine, V1Volume } from '@kubevirt-ui/kubevirt-api/kubevirt';
import { getName } from '@kubevirt-utils/resources/shared';
import { getDisks } from '@kubevirt-utils/resources/vm';
import { NO_DATA_DASH } from '@kubevirt-utils/resources/vm/utils/constants';
import { getPrintableDiskDrive } from '@kubevirt-utils/resources/vm/utils/disk/selectors';
import { convertToBaseValue, humanizeBinaryBytes } from '@kubevirt-utils/utils/humanize.js';
import { isEmpty } from '@kubevirt-utils/utils/utils';

export const getTableDiskData = (
vm: V1VirtualMachine,
volume: V1Volume,
pvcs: IoK8sApiCoreV1PersistentVolumeClaim[],
) => {
const volumeDisk = getDisks(vm)?.find((disk) => disk.name === volume.name);
const volumePVC = pvcs?.find(
(pvc) =>
getName(pvc) === volume.dataVolume?.name ||
getName(pvc) === volume.persistentVolumeClaim?.claimName,
);

const pvcSize = humanizeBinaryBytes(
convertToBaseValue(volumePVC?.spec?.resources?.requests?.storage),
);

return {
drive: getPrintableDiskDrive(volumeDisk),
isSelectable: !volumeDisk?.shareable && !isEmpty(volumePVC),
name: volume.name,
pvc: volumePVC,
size: pvcSize?.value === 0 ? NO_DATA_DASH : pvcSize?.string,
storageClass: volumePVC?.spec?.storageClassName,
};
};
21 changes: 15 additions & 6 deletions src/views/virtualmachines/migrate/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ const getBlankDataVolume = (
};

const createBlankDataVolumes = async (
originPVCs: IoK8sApiCoreV1PersistentVolumeClaim[],
selectedPVCs: IoK8sApiCoreV1PersistentVolumeClaim[],
destinationStorageClass: string,
): Promise<Map<string, V1beta1DataVolume>> => {
const dataVolumesCreationResults = await Promise.allSettled(
originPVCs
selectedPVCs
.map((pvc) => {
return getBlankDataVolume(
getName(pvc),
Expand Down Expand Up @@ -93,17 +93,23 @@ const createBlankDataVolumes = async (

const dataVolumesCreated = fulfilledRequests.map((result) => result.value);

return originPVCs.reduce((createdDataVolumeByPVCName, pvc, index) => {
return selectedPVCs.reduce((createdDataVolumeByPVCName, pvc, index) => {
createdDataVolumeByPVCName.set(getName(pvc), dataVolumesCreated[index]);
return createdDataVolumeByPVCName;
}, new Map<string, V1beta1DataVolume>());
};

const createPatchData = (
vm: V1VirtualMachine,
selectedPVCs: IoK8sApiCoreV1PersistentVolumeClaim[],
createdDataVolumeByPVCName: Map<string, V1beta1DataVolume>,
) =>
getVolumes(vm).reduce((patchArray, volume, volumeIndex) => {
const pvcName = volume?.persistentVolumeClaim?.claimName || volume?.dataVolume?.name;
const pvc = selectedPVCs.find((selectedPVC) => getName(selectedPVC) === pvcName);

if (isEmpty(pvc)) return patchArray;

if (volume?.persistentVolumeClaim?.claimName) {
patchArray.push({
op: 'replace',
Expand Down Expand Up @@ -138,15 +144,15 @@ const createPatchData = (

export const migrateVM = async (
vm: V1VirtualMachine,
originPVCs: IoK8sApiCoreV1PersistentVolumeClaim[],
selectedPVCs: IoK8sApiCoreV1PersistentVolumeClaim[],
destinationStorageClass: string,
) => {
const createdDataVolumeByPVCName = await createBlankDataVolumes(
originPVCs,
selectedPVCs,
destinationStorageClass,
);

const patchData = createPatchData(vm, createdDataVolumeByPVCName);
const patchData = createPatchData(vm, selectedPVCs, createdDataVolumeByPVCName);

patchData.push({
op: 'add',
Expand All @@ -171,3 +177,6 @@ export const migrateVM = async (
throw error;
}
};

export const entireVMSelected = (selectedPVCs: IoK8sApiCoreV1PersistentVolumeClaim[]) =>
selectedPVCs === null;

0 comments on commit 8366043

Please sign in to comment.