Skip to content

Commit

Permalink
Add pause/unpause VM feature
Browse files Browse the repository at this point in the history
  • Loading branch information
pcbailey committed Jan 21, 2020
1 parent 789499a commit d32edd2
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 9 deletions.
@@ -0,0 +1 @@
export * from './vm-status-modal';
@@ -0,0 +1,58 @@
import * as React from 'react';
import { Modal } from '@patternfly/react-core';
import { HandlePromiseProps, withHandlePromise } from '@console/internal/components/utils';
import { ModalComponentProps } from '@console/internal/components/factory';
import { ModalFooter } from '../modal/modal-footer';
import { PAUSED_VM_MODAL_MESSAGE } from '../../../constants/vm';
import { VMIKind } from '../../../types';
import { unpauseVM } from '../../../k8s/requests/vmi/actions';

const modalTitle = 'Edit pause state';

const VMStatusModal = withHandlePromise<VMStatusModalProps>(
({ vmi, isOpen, setOpen, title = modalTitle, handlePromise, inProgress, errorMessage }) => {
const [showPatchError, setPatchError] = React.useState<boolean>(false);

const onSubmit = async (event) => {
event.preventDefault();

const promise = unpauseVM(vmi);
handlePromise(promise)
.then(() => setOpen(false))
.catch(() => setPatchError(true));
};

const footer = (
<ModalFooter
errorMessage={showPatchError && errorMessage}
inProgress={inProgress}
onSubmit={onSubmit}
onCancel={() => setOpen(false)}
submitButtonText="Unpause"
/>
);

return (
<Modal
title={title}
isOpen={isOpen}
isSmall
onClose={() => setOpen(false)}
footer={footer}
isFooterLeftAligned
>
<div>{PAUSED_VM_MODAL_MESSAGE}</div>
</Modal>
);
},
);

export type VMStatusModalProps = HandlePromiseProps &
ModalComponentProps & {
vmi: VMIKind;
title?: string;
isOpen: boolean;
setOpen: (isOpen: boolean) => void;
};

export default VMStatusModal;
@@ -1,6 +1,6 @@
import * as React from 'react';
import { PodKind, K8sResourceKind } from '@console/internal/module/k8s';
import { OffIcon, UnknownIcon, SyncAltIcon } from '@patternfly/react-icons';
import { OffIcon, PausedIcon, SyncAltIcon, UnknownIcon } from '@patternfly/react-icons';
import {
PopoverStatus,
StatusIconAndText,
Expand All @@ -10,12 +10,24 @@ import {
ProgressStatus,
PendingStatus,
} from '@console/shared';
import { Progress, ProgressVariant, ProgressSize } from '@patternfly/react-core';
import {
Progress,
ProgressVariant,
ProgressSize,
Button,
ButtonVariant,
} from '@patternfly/react-core';
import { Link } from 'react-router-dom';
import { resourcePath } from '@console/internal/components/utils';
import { PodModel } from '@console/internal/models';
import { unpauseVM } from '../../k8s/requests/vmi/actions';
import { VirtualMachineModel } from '../../models';
import { VM_DETAIL_EVENTS_HREF, CDI_KUBEVIRT_IO, STORAGE_IMPORT_PVC_NAME } from '../../constants';
import {
VM_DETAIL_EVENTS_HREF,
CDI_KUBEVIRT_IO,
STORAGE_IMPORT_PVC_NAME,
PAUSED_VM_MODAL_MESSAGE,
} from '../../constants';
import { getLabels } from '../../selectors/selectors';
import { getVMStatus } from '../../statuses/vm/vm';
import {
Expand All @@ -33,6 +45,7 @@ import {
VM_STATUS_OFF,
VM_STATUS_ERROR,
VM_STATUS_IMPORT_PENDING,
VM_STATUS_PAUSED,
} from '../../statuses/vm/constants';
import { VMKind, VMIKind } from '../../types';

Expand Down Expand Up @@ -102,6 +115,11 @@ export const VMStatus: React.FC<VMStatusProps> = ({
)}`; // to default tab
const additionalText = verbose ? getAdditionalImportText(statusDetail.pod) : null;

const unpauseVMI = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
await unpauseVM(vmi);
};

switch (statusDetail.status) {
case VM_STATUS_V2V_CONVERSION_PENDING:
return (
Expand Down Expand Up @@ -248,6 +266,20 @@ export const VMStatus: React.FC<VMStatusProps> = ({
/>
</ProgressStatus>
);
case VM_STATUS_PAUSED:
return (
<PopoverStatus title="Paused" icon={<PausedIcon />}>
<VMStatusPopoverContent message={PAUSED_VM_MODAL_MESSAGE}>
<Button
variant={ButtonVariant.primary}
onClick={unpauseVMI}
id="paused-popover-submit"
>
Unpause
</Button>
</VMStatusPopoverContent>
</PopoverStatus>
);
case VM_STATUS_RUNNING:
return <StatusIconAndText title="Running" icon={<SyncAltIcon />} />;
case VM_STATUS_OFF:
Expand Down
@@ -1,7 +1,7 @@
import * as _ from 'lodash';
import * as React from 'react';
import { asAccessReview, Kebab, KebabOption } from '@console/internal/components/utils';
import { K8sKind, K8sResourceKind, PodKind } from '@console/internal/module/k8s';
import { K8sKind, K8sResourceCommon, K8sResourceKind, PodKind } from '@console/internal/module/k8s';
import { getName, getNamespace } from '@console/shared';
import { confirmModal } from '@console/internal/components/modals';
import { VMIKind, VMKind } from '../../types/vm';
Expand All @@ -15,17 +15,19 @@ import { cancelMigration } from '../../k8s/requests/vmim';
import { cloneVMModal } from '../modals/clone-vm-modal';
import { VMCDRomModal } from '../modals/cdrom-vm-modal';
import { getVMStatus } from '../../statuses/vm/vm';
import { isVMIPaused } from '../../selectors/vmi';
import { unpauseVM, VMIActionType } from '../../k8s/requests/vmi/actions';

type ActionArgs = {
migration?: K8sResourceKind;
vmi?: VMIKind;
vmStatus?: VMMultiStatus;
};

const getVMActionMessage = (vm, action: VMActionType) => (
const getVMActionMessage = (obj: K8sResourceCommon, action: VMActionType | VMIActionType) => (
<>
Are you sure you want to {action} <strong>{getName(vm)}</strong> in namespace{' '}
<strong>{getNamespace(vm)}</strong>?
Are you sure you want to {action} <strong>{getName(obj)}</strong> in namespace{' '}
<strong>{getNamespace(obj)}</strong>?
</>
);

Expand Down Expand Up @@ -85,6 +87,21 @@ const menuActionRestart = (
};
};

const menuActionUnpause = (kindObj: K8sKind, vm: VMKind, { vmi }: ActionArgs): KebabOption => {
const title = 'Unpause Virtual Machine';
return {
hidden: !isVMIPaused(vmi),
label: title,
callback: () =>
confirmModal({
title,
message: getVMActionMessage(vmi, VMIActionType.Unpause),
btnText: _.capitalize(VMIActionType.Unpause),
executeFn: () => unpauseVM(vmi),
}),
};
};

const menuActionMigrate = (
kindObj: K8sKind,
vm: VMKind,
Expand Down Expand Up @@ -156,6 +173,7 @@ export const vmMenuActions = [
menuActionStart,
menuActionStop,
menuActionRestart,
menuActionUnpause,
menuActionMigrate,
menuActionCancelMigration,
menuActionClone,
Expand Down
Expand Up @@ -9,6 +9,7 @@ import { getBasicID, prefixedID } from '../../utils';
import { vmDescriptionModal, vmFlavorModal } from '../modals';
import { VMCDRomModal } from '../modals/cdrom-vm-modal';
import { BootOrderModal } from '../modals/boot-order-modal/boot-order-modal';
import VMStatusModal from '../modals/vm-status-modal/vm-status-modal';
import { getDescription } from '../../selectors/selectors';
import { getCDRoms } from '../../selectors/vm/selectors';
import { getVMTemplateNamespacedName } from '../../selectors/vm-template/selectors';
Expand All @@ -26,8 +27,8 @@ import {
getWorkloadProfile,
getDevices,
} from '../../selectors/vm';

import './vm-resource.scss';
import { isVMIPaused } from '../../selectors/vmi';

export const VMDetailsItem: React.FC<VMDetailsItemProps> = ({
title,
Expand Down Expand Up @@ -94,6 +95,7 @@ export const VMDetailsList: React.FC<VMResourceListProps> = ({
canUpdateVM,
}) => {
const [isBootOrderModalOpen, setBootOrderModalOpen] = React.useState<boolean>(false);
const [isStatusModalOpen, setStatusModalOpen] = React.useState<boolean>(false);

const id = getBasicID(vm);
const vmStatus = getVMStatus({ vm, vmi, pods, migrations });
Expand All @@ -107,7 +109,14 @@ export const VMDetailsList: React.FC<VMResourceListProps> = ({

return (
<dl className="co-m-pane__details">
<VMDetailsItem title="Status" idValue={prefixedID(id, 'vm-statuses')}>
<VMDetailsItem
title="Status"
canEdit={isVMIPaused(vmi)}
editButtonId={prefixedID(id, 'status-edit')}
onEditClick={() => setStatusModalOpen(true)}
idValue={prefixedID(id, 'vm-statuses')}
>
<VMStatusModal isOpen={isStatusModalOpen} setOpen={setStatusModalOpen} vmi={vmi} />
<VMStatuses vm={vm} vmi={vmi} pods={pods} migrations={migrations} />
</VMDetailsItem>

Expand Down
Expand Up @@ -42,3 +42,6 @@ export enum DeviceType {
}

export const VM_DETAIL_EVENTS_HREF = 'events';

export const PAUSED_VM_MODAL_MESSAGE =
'This VM has been paused. If you wish to unpause it, please click the Unpause button below. For further details, please check with your system administrator.';
32 changes: 32 additions & 0 deletions frontend/packages/kubevirt-plugin/src/k8s/requests/vmi/actions.ts
@@ -0,0 +1,32 @@
import { coFetch } from '@console/internal/co-fetch';
import { resourceURL } from '@console/internal/module/k8s';
import { getName, getNamespace } from '@console/shared';
import { VirtualMachineInstanceModel } from '../../../models';
import { VMIKind } from '../../../types/vm';

export enum VMIActionType {
Unpause = 'unpause',
}

const VMIActionRequest = async (vm: VMIKind, action: VMIActionType) => {
const method = 'PUT';
let url = resourceURL(
{
...VirtualMachineInstanceModel,
apiGroup: `subresources.${VirtualMachineInstanceModel.apiGroup}`,
},
{
ns: getNamespace(vm),
name: getName(vm),
},
);

url = `${url}/${action}`;

const response = await coFetch(url, { method });
const text = await response.text();

return text;
};

export const unpauseVM = async (vmi: VMIKind) => VMIActionRequest(vmi, VMIActionType.Unpause);
3 changes: 3 additions & 0 deletions frontend/packages/kubevirt-plugin/src/selectors/vmi/basic.ts
Expand Up @@ -26,3 +26,6 @@ export const getVMIInterfaces = (vmi: VMIKind) =>
(vmi && vmi.status && vmi.status.interfaces) || [];

export const getVMINodeName = (vmi: VMIKind) => vmi && vmi.status && vmi.status.nodeName;

export const isVMIPaused = (vmi: VMIKind): boolean =>
getVMIConditionsByType(vmi, 'Paused').length > 0;
Expand Up @@ -4,6 +4,7 @@ export const VM_STATUS_STARTING = 'VM_STATUS_STARTING';
export const VM_STATUS_VMI_WAITING = 'VM_STATUS_VMI_WAITING';
export const VM_STATUS_IMPORTING = 'VM_STATUS_IMPORTING';
export const VM_STATUS_STOPPING = 'VM_STATUS_STOPPING';
export const VM_STATUS_PAUSED = 'VM_STATUS_PAUSED';

export const VM_STATUS_V2V_CONVERSION_IN_PROGRESS = 'VM_STATUS_CONVERSION_IN_PROGRESS';
export const VM_STATUS_V2V_CONVERSION_PENDING = 'VM_STATUS_CONVERSION_PENDING';
Expand Down
6 changes: 6 additions & 0 deletions frontend/packages/kubevirt-plugin/src/statuses/vm/vm.ts
Expand Up @@ -46,8 +46,10 @@ import {
VM_STATUS_V2V_CONVERSION_PENDING,
CONVERSION_PROGRESS_ANNOTATION,
VM_STATUS_IMPORT_PENDING,
VM_STATUS_PAUSED,
} from './constants';
import { Status } from '..';
import { isVMIPaused } from '../../selectors/vmi/basic';

const isBeingMigrated = (vm: VMKind, migrations?: K8sResourceKind[]): VMStatus => {
const migration = findVMIMigration(vm, migrations);
Expand Down Expand Up @@ -96,6 +98,9 @@ const isReady = (vmi: VMIKind, launcherPod: PodKind): VMStatus => {
return NOT_HANDLED;
};

const isPaused = (vmi: VMIKind): VMStatus =>
isVMIPaused(vmi) ? { status: VM_STATUS_PAUSED } : NOT_HANDLED;

const isVMError = (vm: VMKind): VMStatus => {
// is an issue with the VM definition?
const condition = getVMStatusConditions(vm)[0];
Expand Down Expand Up @@ -237,6 +242,7 @@ export const getVMStatus = ({
}): VMStatus => {
const launcherPod = findVMPod(vm, pods);
return (
isPaused(vmi) ||
isV2VConversion(vm, pods) || // these statuses must precede isRunning() because they do not rely on ready vms
isBeingMigrated(vm, migrations) || // -||-
isBeingImported(vm, pods) || // -||-
Expand Down

0 comments on commit d32edd2

Please sign in to comment.