Skip to content

Commit

Permalink
Merge pull request #2242 from upalatucci/create-bootable-volume-from-…
Browse files Browse the repository at this point in the history
…disj

CNV-48279: Create bootable volume from disk
  • Loading branch information
openshift-merge-bot[bot] authored Nov 11, 2024
2 parents bfe9231 + b70ef9e commit a1c6775
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 16 deletions.
1 change: 1 addition & 0 deletions locales/en/plugin__kubevirt-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,7 @@
"SATA": "SATA",
"Save": "Save",
"Save affinity rule": "Save affinity rule",
"Save as bootable volume": "Save as bootable volume",
"Save memory with OpenShift Virtualization using Free Page Reporting": "Save memory with OpenShift Virtualization using Free Page Reporting",
"Saved": "Saved",
"Schedule specifies in cron format when and how often to look for new imports.": "Schedule specifies in cron format when and how often to look for new imports.",
Expand Down
26 changes: 13 additions & 13 deletions src/utils/components/AddBootableVolumeModal/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,23 @@ export const optionsValueLabelMapper = {
};

export type AddBootableVolumeState = {
annotations: { [key: string]: string };
annotations?: { [key: string]: string };
bootableVolumeName: string;
bootableVolumeNamespace: string;
cronExpression: string;
isIso: boolean;
labels: { [key: string]: string };
cronExpression?: string;
isIso?: boolean;
labels?: { [key: string]: string };
pvcName: string;
pvcNamespace: string;
registryURL: string;
retainRevisions: number;
size: string;
snapshotName: string;
snapshotNamespace: string;
storageClassName: string;
storageClassProvisioner: string;
uploadFile: File | string;
uploadFilename: string;
registryURL?: string;
retainRevisions?: number;
size?: string;
snapshotName?: string;
snapshotNamespace?: string;
storageClassName?: string;
storageClassProvisioner?: string;
uploadFile?: File | string;
uploadFilename?: string;
};

export type SetBootableVolumeFieldType = (
Expand Down
12 changes: 9 additions & 3 deletions src/utils/components/AddBootableVolumeModal/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { buildOwnerReference } from '@kubevirt-utils/resources/shared';
import { DATA_SOURCE_CRONJOB_LABEL } from '@kubevirt-utils/resources/template';
import { ClaimPropertySets } from '@kubevirt-utils/types/storage';
import { appendDockerPrefix, getRandomChars } from '@kubevirt-utils/utils/utils';
import { k8sCreate } from '@openshift-console/dynamic-plugin-sdk';
import { k8sCreate, k8sDelete } from '@openshift-console/dynamic-plugin-sdk';

import {
AddBootableVolumeState,
Expand Down Expand Up @@ -193,7 +193,7 @@ const createSnapshotDataSource = async (
return await k8sCreate({ data: dataSourceToCreate, model: DataSourceModel });
};

const createPVCBootableVolume = async (
export const createPVCBootableVolume = async (
bootableVolume: AddBootableVolumeState,
namespace: string,
applyStorageProfileSettings: boolean,
Expand All @@ -220,7 +220,13 @@ const createPVCBootableVolume = async (
});

const createdDS = await k8sCreate({ data: dataSourceToCreate, model: DataSourceModel });
await k8sCreate({ data: bootableVolumeToCreate, model: DataVolumeModel });

try {
await k8sCreate({ data: bootableVolumeToCreate, model: DataVolumeModel });
} catch (error) {
k8sDelete({ model: DataSourceModel, resource: createdDS });
throw error;
}
return createdDS;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React, { FC, useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
import xbytes from 'xbytes';

import {
DEFAULT_INSTANCETYPE_LABEL,
DEFAULT_PREFERENCE_LABEL,
} from '@catalog/CreateFromInstanceTypes/utils/constants';
import DataSourceModel from '@kubevirt-ui/kubevirt-api/console/models/DataSourceModel';
import { V1beta1DataSource } from '@kubevirt-ui/kubevirt-api/containerized-data-importer/models';
import { IoK8sApiCoreV1PersistentVolumeClaim } from '@kubevirt-ui/kubevirt-api/kubernetes';
import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt';
import VolumeDestination from '@kubevirt-utils/components/AddBootableVolumeModal/components/VolumeDestination/VolumeDestination';
import VolumeMetadata from '@kubevirt-utils/components/AddBootableVolumeModal/components/VolumeMetadata/VolumeMetadata';
import {
AddBootableVolumeState,
initialBootableVolumeState,
SetBootableVolumeFieldType,
} from '@kubevirt-utils/components/AddBootableVolumeModal/utils/constants';
import { removeByteSuffix } from '@kubevirt-utils/components/CapacityInput/utils';
import HelpTextIcon from '@kubevirt-utils/components/HelpTextIcon/HelpTextIcon';
import TabModal from '@kubevirt-utils/components/TabModal/TabModal';
import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import useStorageProfileClaimPropertySets from '@kubevirt-utils/hooks/useStorageProfileClaimPropertySets';
import { modelToGroupVersionKind, PersistentVolumeClaimModel } from '@kubevirt-utils/models';
import { getName, getNamespace, getResourceUrl } from '@kubevirt-utils/resources/shared';
import { getInstanceTypeMatcher, getPreferenceMatcher } from '@kubevirt-utils/resources/vm';
import { NO_DATA_DASH } from '@kubevirt-utils/resources/vm/utils/constants';
import { DiskRowDataLayout } from '@kubevirt-utils/resources/vm/utils/disk/constants';
import { hasSizeUnit } from '@kubevirt-utils/resources/vm/utils/disk/size';
import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk';
import { PopoverPosition, Stack, Title } from '@patternfly/react-core';

import { createBootableVolumeFromDisk } from './utils';

type CreateBootableVolumeModalProps = {
diskObj: DiskRowDataLayout;
isOpen: boolean;
onClose: () => void;
vm: V1VirtualMachine;
};

const CreateBootableVolumeModal: FC<CreateBootableVolumeModalProps> = ({
diskObj,
isOpen,
onClose,
vm,
}) => {
const { t } = useKubevirtTranslation();
const navigate = useNavigate();

const [pvc, pvcLoaded] = useK8sWatchResource<IoK8sApiCoreV1PersistentVolumeClaim>({
groupVersionKind: modelToGroupVersionKind(PersistentVolumeClaimModel),
name: diskObj.source,
namespace: diskObj?.namespace,
});

const [bootableVolume, setBootableVolume] = useState<AddBootableVolumeState>({
...initialBootableVolumeState,
bootableVolumeName: `${getName(vm)}-${diskObj.name}`,
bootableVolumeNamespace: getNamespace(vm),
labels: {
[DEFAULT_INSTANCETYPE_LABEL]: getInstanceTypeMatcher(vm)?.name,
[DEFAULT_PREFERENCE_LABEL]: getPreferenceMatcher(vm)?.name,
},
pvcName: diskObj.source,
pvcNamespace: diskObj?.namespace,
storageClassName: diskObj?.storageClass === NO_DATA_DASH ? null : diskObj?.storageClass,
});
const applyStorageProfileState = useState<boolean>(true);

const claimPropertySetsData = useStorageProfileClaimPropertySets(
bootableVolume?.storageClassName,
);

const setBootableVolumeField: SetBootableVolumeFieldType = useCallback(
(key, fieldKey) => (value) =>
setBootableVolume((prevState) => ({
...prevState,
...(fieldKey
? { [key]: { ...(prevState[key] as object), [fieldKey]: value } }
: { ...prevState, [key]: value }),
})),
[],
);

const deleteLabel = (labelKey: string) => {
setBootableVolume((prev) => {
const updatedLabels = { ...prev?.labels };
delete updatedLabels[labelKey];

return { ...prev, labels: updatedLabels };
});
};

const onSubmit = async () => {
const createdDS = await createBootableVolumeFromDisk(
diskObj,
vm,
bootableVolume,
applyStorageProfileState[0],
claimPropertySetsData.claimPropertySets,
);

navigate(getResourceUrl({ model: DataSourceModel, resource: createdDS }));
};

useEffect(() => {
if (!pvcLoaded) return;

const pvcSize = pvc?.spec?.resources?.requests?.storage;

const newBootSize = hasSizeUnit(pvcSize)
? pvcSize
: removeByteSuffix(xbytes(Number(pvcSize), { iec: true, space: false }));

setBootableVolumeField('size')(newBootSize);
}, [pvc, pvcLoaded, setBootableVolumeField]);

return (
<TabModal<V1beta1DataSource>
headerText={t('Save as bootable volume')}
isDisabled={!bootableVolume?.labels?.[DEFAULT_PREFERENCE_LABEL]}
isOpen={isOpen}
onClose={onClose}
onSubmit={onSubmit}
submitBtnText={t('Save')}
>
<Stack hasGutter>
<Title className="pf-u-mt-md" headingLevel="h5">
{t('Destination details')}
</Title>
<VolumeDestination
applyStorageProfileState={applyStorageProfileState}
bootableVolume={bootableVolume}
claimPropertySetsData={claimPropertySetsData}
setBootableVolumeField={setBootableVolumeField}
/>
<Title className="pf-u-mt-md" headingLevel="h5">
{t('Volume metadata')}{' '}
<HelpTextIcon
bodyContent={t('Set the volume metadata to use the volume as a bootable image.')}
helpIconClassName="add-bootable-volume-modal__title-help-text-icon"
position={PopoverPosition.right}
/>
</Title>
<VolumeMetadata
bootableVolume={bootableVolume}
deleteLabel={deleteLabel}
setBootableVolumeField={setBootableVolumeField}
/>
</Stack>
</TabModal>
);
};

export default CreateBootableVolumeModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import produce from 'immer';

import { DEFAULT_PREFERENCE_LABEL } from '@catalog/CreateFromInstanceTypes/utils/constants';
import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt';
import {
AddBootableVolumeState,
emptyDataSource,
} from '@kubevirt-utils/components/AddBootableVolumeModal/utils/constants';
import { createPVCBootableVolume } from '@kubevirt-utils/components/AddBootableVolumeModal/utils/utils';
import { getPreferenceMatcher } from '@kubevirt-utils/resources/vm';
import { DiskRowDataLayout } from '@kubevirt-utils/resources/vm/utils/disk/constants';
import { ClaimPropertySets } from '@kubevirt-utils/types/storage';

export const createBootableVolumeFromDisk = async (
diskObj: DiskRowDataLayout,
vm: V1VirtualMachine,
bootableVolumeSource: AddBootableVolumeState,
applyStorageProfileSettings: boolean,
claimPropertySets: ClaimPropertySets,
) => {
const dataSource = produce(emptyDataSource, (draftDataSource) => {
draftDataSource.metadata.name = bootableVolumeSource.bootableVolumeName;
draftDataSource.metadata.namespace = bootableVolumeSource.bootableVolumeNamespace;

draftDataSource.metadata.labels = {
[DEFAULT_PREFERENCE_LABEL]: getPreferenceMatcher(vm)?.name,
...(bootableVolumeSource.labels || {}),
};
});

return createPVCBootableVolume(
bootableVolumeSource,
diskObj?.namespace,
applyStorageProfileSettings,
claimPropertySets,
dataSource,
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ButtonVariant, Dropdown, DropdownItem, DropdownList } from '@patternfly
import { updateDisks } from '@virtualmachines/details/tabs/configuration/details/utils/utils';
import { isRunning } from '@virtualmachines/utils';

import CreateBootableVolumeModal from '../../modal/CreateBootableVolumeModal';
import DeleteDiskModal from '../../modal/DeleteDiskModal';
import DetachModal from '../../modal/DetachModal';
import MakePersistentModal from '../../modal/MakePersistentModal';
Expand All @@ -38,6 +39,7 @@ const DiskRowActions: FC<DiskRowActionsProps> = ({
}) => {
const { t } = useKubevirtTranslation();
const { createModal } = useModal();

const [isDropdownOpen, setIsDropdownOpen] = useState(false);

const diskName = obj?.name;
Expand Down Expand Up @@ -109,6 +111,13 @@ const DiskRowActions: FC<DiskRowActionsProps> = ({
/>
),
);

const createBootableVolume = () => {
createModal(({ isOpen, onClose }) => (
<CreateBootableVolumeModal diskObj={obj} isOpen={isOpen} onClose={onClose} vm={vm} />
));
};

const makePersistent = () =>
createModal(({ isOpen, onClose }) => (
<MakePersistentModal isOpen={isOpen} onClose={onClose} vm={vm} volume={volume} />
Expand Down Expand Up @@ -137,6 +146,13 @@ const DiskRowActions: FC<DiskRowActionsProps> = ({
>
{t('Upload to registry')}
</DropdownItem>
<DropdownItem
isDisabled={!isPVCSource(obj)}
key="disk-bootable-volume"
onClick={() => createBootableVolume()}
>
{t('Save as bootable volume')}
</DropdownItem>
<DropdownItem key="disk-edit" onClick={() => onModalOpen(createEditDiskModal)}>
{editBtnText}
</DropdownItem>
Expand Down

0 comments on commit a1c6775

Please sign in to comment.