Skip to content

Commit

Permalink
Merge pull request #2368 from upalatucci/rollback-migration
Browse files Browse the repository at this point in the history
CNV-55113: rollback migration
  • Loading branch information
openshift-merge-bot[bot] authored Jan 21, 2025
2 parents 19fa843 + f863947 commit 8b263ac
Show file tree
Hide file tree
Showing 11 changed files with 214 additions and 18 deletions.
2 changes: 2 additions & 0 deletions locales/en/plugin__kubevirt-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,10 @@
"Can not delete in view-only mode": "Can not delete in view-only mode",
"Can not edit in view-only mode": "Can not edit in view-only mode",
"Cancel": "Cancel",
"Cancel compute migration": "Cancel compute migration",
"Cancel error": "Cancel error",
"Cancel migration": "Cancel migration",
"Cancel storage migration": "Cancel storage migration",
"Cancel upload": "Cancel upload",
"Cancel Upload": "Cancel Upload",
"Canceled": "Canceled",
Expand Down
4 changes: 4 additions & 0 deletions src/utils/resources/vm/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ export const DEFAULT_NETWORK_INTERFACE: V1Interface = { masquerade: {}, name: 'd
export const DEFAULT_NETWORK: V1Network = { name: 'default', pod: {} };

export const UDN_BINDING_NAME = 'l2bridge';

export enum UPDATE_STRATEGIES {
Migration = 'Migration',
}
4 changes: 4 additions & 0 deletions src/utils/resources/vm/utils/dataVolumeTemplate/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { V1beta1DataVolume } from '@kubevirt-ui/kubevirt-api/containerized-data-importer/models';

export const getStorageClassName = (dataVolume: V1beta1DataVolume): string =>
dataVolume?.spec?.storage?.storageClassName;
4 changes: 4 additions & 0 deletions src/utils/resources/vm/utils/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { getAnnotation, getLabel } from '@kubevirt-utils/resources/shared';
import { WORKLOADS } from '@kubevirt-utils/resources/template';

import { VM_WORKLOAD_ANNOTATION } from './annotations';
import { UPDATE_STRATEGIES } from './constants';

/**
* A selector for the virtual machine's networks
Expand Down Expand Up @@ -272,3 +273,6 @@ export const getPreferenceMatcher = (vm: V1VirtualMachine): V1PreferenceMatcher
*/
export const getStatusConditions = (vm: V1VirtualMachine): V1VirtualMachineCondition[] =>
vm?.status?.conditions;

export const getUpdateStrategy = (vm: V1VirtualMachine): UPDATE_STRATEGIES =>
vm?.spec?.updateVolumesStrategy as UPDATE_STRATEGIES;
26 changes: 24 additions & 2 deletions src/views/virtualmachines/actions/VirtualMachineActionFactory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
migrateVM,
pauseVM,
restartVM,
rollbackStorageMigration,
startVM,
stopVM,
unpauseVM,
Expand All @@ -54,7 +55,7 @@ const {
} = printableVMStatus;

export const VirtualMachineActionFactory = {
cancelMigrationCompute: (
cancelComputeMigration: (
vm: V1VirtualMachine,
vmim: V1VirtualMachineInstanceMigration,
isSingleNodeCluster: boolean,
Expand All @@ -70,7 +71,28 @@ export const VirtualMachineActionFactory = {
description: !!vmim?.metadata?.deletionTimestamp && t('Canceling ongoing migration'),
disabled: isSingleNodeCluster || !vmim || !!vmim?.metadata?.deletionTimestamp,
id: 'vm-action-cancel-migrate',
label: t('Cancel migration'),
label: t('Cancel compute migration'),
};
},
cancelStorageMigration: (
vm: V1VirtualMachine,
vmim: V1VirtualMachineInstanceMigration,
isSingleNodeCluster: boolean,
): Action => {
return {
accessReview: {
group: VirtualMachineModel.apiGroup,
namespace: vm?.metadata?.namespace,
resource: VirtualMachineModel.plural,
verb: 'patch',
},
cta: () => {
cancelMigration(vmim);
return rollbackStorageMigration(vm);
},
disabled: isSingleNodeCluster,
id: 'vm-action-cancel-storage-migrate',
label: t('Cancel storage migration'),
};
},
clone: (vm: V1VirtualMachine, createModal: (modal: ModalComponent) => void): Action => {
Expand Down
27 changes: 26 additions & 1 deletion src/views/virtualmachines/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,20 @@ import {
V1VirtualMachine,
V1VirtualMachineInstanceMigration,
} from '@kubevirt-ui/kubevirt-api/kubevirt';
import { kubevirtConsole } from '@kubevirt-utils/utils/utils';
import { getNamespace } from '@kubevirt-utils/resources/shared';
import { getUpdateStrategy } from '@kubevirt-utils/resources/vm';
import { isEmpty, kubevirtConsole } from '@kubevirt-utils/utils/utils';
import {
consoleFetch,
k8sCreate,
k8sDelete,
K8sModel,
k8sPatch,
Patch,
} from '@openshift-console/dynamic-plugin-sdk';

import { createRollbackPatchData, deleteUnusedDataVolumes } from './utils';

const generateRandomString = () => Math.random().toString(36).substring(2, 7);

export enum VMActionType {
Expand Down Expand Up @@ -110,3 +116,22 @@ export const deleteVM = async (vm: V1VirtualMachine) => {
resource: vm,
});
};

export const rollbackStorageMigration = async (vm: V1VirtualMachine) => {
if (isEmpty(getUpdateStrategy(vm))) return;

const patchData: Patch[] = createRollbackPatchData(vm);

patchData.push({
op: 'remove',
path: '/spec/updateVolumesStrategy',
});

await k8sPatch<V1VirtualMachine>({
data: patchData,
model: VirtualMachineModel,
resource: vm,
});

deleteUnusedDataVolumes(vm, getNamespace(vm));
};
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const VirtualMachineMigrationDetails: FC<VirtualMachineMigrationDetailsProps> =
isChecked={!allVolumes}
label={t('Selected volumes')}
name="volumes"
onChange={() => setSelectedPVCs(pvcs)}
onChange={() => setSelectedPVCs([])}
/>
</StackItem>
{!allVolumes && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import { V1VirtualMachine, V1Volume } from '@kubevirt-ui/kubevirt-api/kubevirt';
import { MAX_NAME_LENGTH } from '@kubevirt-utils/components/SSHSecretModal/utils/constants';
import { getName, getNamespace } from '@kubevirt-utils/resources/shared';
import { getDataVolumeTemplates, getVolumes } from '@kubevirt-utils/resources/vm';
import { UPDATE_STRATEGIES } from '@kubevirt-utils/resources/vm/utils/constants';
import { getStorageClassName } from '@kubevirt-utils/resources/vm/utils/dataVolumeTemplate/selectors';
import { getRandomChars, isEmpty } from '@kubevirt-utils/utils/utils';
import { k8sCreate, k8sDelete, k8sPatch, Patch } from '@openshift-console/dynamic-plugin-sdk';
import { getMigrationClaimNameAnnotation } from '@virtualmachines/actions/utils';

const getBlankDataVolume = (
dataVolumeName: string,
Expand Down Expand Up @@ -106,34 +109,51 @@ const createPatchData = (

if (isEmpty(pvc)) return patchArray;

const destination = createdDataVolumeByPVCName.get(pvcName);

const destinationName = getName(destination);

const migrationClaimNameAnnotation = getMigrationClaimNameAnnotation(destinationName);

patchArray.push({
op: 'add',
path: `/metadata/annotations/${migrationClaimNameAnnotation.replace('/', '~1')}`,
value: pvcName,
});

if (volume?.persistentVolumeClaim?.claimName) {
patchArray.push({
op: 'replace',
path: `/spec/template/spec/volumes/${volumeIndex}/persistentVolumeClaim/claimName`,
value: getName(createdDataVolumeByPVCName.get(volume?.persistentVolumeClaim?.claimName)),
value: destinationName,
});
}

if (volume?.dataVolume?.name) {
const destinationDataVolumeName = getName(
createdDataVolumeByPVCName.get(volume?.dataVolume?.name),
);

const dataVolumeIndex = getDataVolumeTemplates(vm)?.findIndex(
(dataVolumeTemplate) => getName(dataVolumeTemplate) === volume?.dataVolume?.name,
);

patchArray.push({
op: 'replace',
path: `/spec/template/spec/volumes/${volumeIndex}/dataVolume/name`,
value: destinationDataVolumeName,
value: destinationName,
});

patchArray.push({
op: 'replace',
path: `/spec/dataVolumeTemplates/${dataVolumeIndex}/metadata/name`,
value: destinationDataVolumeName,
value: destinationName,
});

const storageClassname = getStorageClassName(destination);

if (storageClassname)
patchArray.push({
op: 'replace',
path: `/spec/dataVolumeTemplates/${dataVolumeIndex}/spec/storage/storageClassName`,
value: storageClassname,
});
}
return patchArray;
}, [] as Patch[]);
Expand All @@ -153,7 +173,7 @@ export const migrateVM = async (
patchData.push({
op: 'add',
path: '/spec/updateVolumesStrategy',
value: 'Migration',
value: UPDATE_STRATEGIES.Migration,
});

try {
Expand Down
1 change: 1 addition & 0 deletions src/views/virtualmachines/actions/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ANNOTATION_PREFIX_MIGRATION_ORIGIN_CLAIMNAME = 'migrate-storage-class/';
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { getConsoleVirtctlCommand } from '@kubevirt-utils/components/SSHAccess/u
import { TREE_VIEW, TREE_VIEW_FOLDERS } from '@kubevirt-utils/hooks/useFeatures/constants';
import { useFeatures } from '@kubevirt-utils/hooks/useFeatures/useFeatures';
import { VirtualMachineModelRef } from '@kubevirt-utils/models';
import { getUpdateStrategy } from '@kubevirt-utils/resources/vm';
import { UPDATE_STRATEGIES } from '@kubevirt-utils/resources/vm/utils/constants';
import { vmimStatuses } from '@kubevirt-utils/resources/vmim/statuses';
import { useK8sModel } from '@openshift-console/dynamic-plugin-sdk';

Expand Down Expand Up @@ -40,9 +42,12 @@ const useVirtualMachineActionsProvider: UseVirtualMachineActionsProvider = (
const printableStatus = vm?.status?.printableStatus;
const { Migrating, Paused } = printableVMStatus;

const isMigrating =
printableStatus === Migrating ||
(vmim && ![vmimStatuses.Failed, vmimStatuses.Succeeded].includes(vmim?.status?.phase));
const currentMigrationExist =
vmim && ![vmimStatuses.Failed, vmimStatuses.Succeeded].includes(vmim?.status?.phase);

const isStorageMigration =
currentMigrationExist && getUpdateStrategy(vm) === UPDATE_STRATEGIES.Migration;
const isComputeMigration = printableStatus === Migrating || currentMigrationExist;

const startOrStop = ((printableStatusMachine) => {
const map = {
Expand All @@ -58,9 +63,14 @@ const useVirtualMachineActionsProvider: UseVirtualMachineActionsProvider = (

const migrateStorage = VirtualMachineActionFactory.migrateStorage(vm, createModal);

const migrationActions = isMigrating
? [VirtualMachineActionFactory.cancelMigrationCompute(vm, vmim, isSingleNodeCluster)]
: [migrateCompute, migrateStorage];
const cancelMigration = isStorageMigration
? VirtualMachineActionFactory.cancelStorageMigration(vm, vmim, isSingleNodeCluster)
: VirtualMachineActionFactory.cancelComputeMigration(vm, vmim, isSingleNodeCluster);

const migrationActions =
isComputeMigration || isStorageMigration
? [cancelMigration]
: [migrateCompute, migrateStorage];

const pauseOrUnpause =
printableStatus === Paused
Expand Down
104 changes: 104 additions & 0 deletions src/views/virtualmachines/actions/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import DataVolumeModel from '@kubevirt-ui/kubevirt-api/console/models/DataVolumeModel';
import { V1beta1DataVolume } from '@kubevirt-ui/kubevirt-api/containerized-data-importer/models';
import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt';
import { getAnnotation, getName } from '@kubevirt-utils/resources/shared';
import { getDataVolumeTemplates, getVolumes } from '@kubevirt-utils/resources/vm';
import { k8sDelete, Patch } from '@openshift-console/dynamic-plugin-sdk';

import { ANNOTATION_PREFIX_MIGRATION_ORIGIN_CLAIMNAME } from './constants';

export const deleteUnusedDataVolumes = (
vm: V1VirtualMachine,
namespace: string,
): Promise<V1beta1DataVolume>[] => {
return getVolumes(vm).reduce((acc, volume) => {
if (volume.dataVolume?.name) {
acc.push(
k8sDelete<V1beta1DataVolume>({
model: DataVolumeModel,
resource: {
apiVersion: `${DataVolumeModel.apiGroup}/${DataVolumeModel.apiVersion}`,
kind: DataVolumeModel.kind,
metadata: {
name: volume.dataVolume?.name,
namespace,
},
} as V1beta1DataVolume,
}),
);
}

return acc;
}, [] as Promise<V1beta1DataVolume>[]);
};

export const getMigrationClaimNameAnnotation = (migrationClaimName: string): string =>
`${ANNOTATION_PREFIX_MIGRATION_ORIGIN_CLAIMNAME}${migrationClaimName}`;

export const createRollbackPatchData = (vm: V1VirtualMachine): Patch[] => {
const vmVolumes = getVolumes(vm);
const vmDataVolumeTemplates = getDataVolumeTemplates(vm);

const dataVolumeRollback = (vmDataVolumeTemplates || []).reduce((acc, dataVolume, dvIndex) => {
const dataVolumeName = getName(dataVolume);

const volumeIndex = vmVolumes?.findIndex(
(volume) => dataVolumeName === volume?.dataVolume?.name,
);

const originalClaimAnnotation = getMigrationClaimNameAnnotation(dataVolumeName);

const originalClaimStorageMigration = getAnnotation(vm, originalClaimAnnotation);

if (originalClaimStorageMigration) {
acc.push(
...[
{
op: 'remove',
path: `/metadata/annotations/${originalClaimAnnotation.replace('/', '~1')}`,
},
{
op: 'replace',
path: `/spec/template/spec/volumes/${volumeIndex}/dataVolume/name`,
value: originalClaimStorageMigration,
},
{
op: 'replace',
path: `/spec/dataVolumeTemplates/${dvIndex}/metadata/name`,
value: originalClaimStorageMigration,
},
],
);
}

return acc;
}, [] as Patch[]);

const pvcRollback = (vmVolumes || []).reduce((acc, volume, volumeIndex) => {
const pvcName = volume.persistentVolumeClaim?.claimName;

const originalClaimAnnotation = getMigrationClaimNameAnnotation(pvcName);

const originalClaimStorageMigration = getAnnotation(vm, originalClaimAnnotation);

if (originalClaimStorageMigration) {
acc.push(
...[
{
op: 'remove',
path: `/metadata/annotations/${originalClaimAnnotation.replace('/', '~1')}`,
},
{
op: 'replace',
path: `/spec/template/spec/volumes/${volumeIndex}/persistentVolumeClaim/claimName`,
value: originalClaimStorageMigration,
},
],
);
}

return acc;
}, [] as Patch[]);

return [...dataVolumeRollback, ...pvcRollback];
};

0 comments on commit 8b263ac

Please sign in to comment.