Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Suspend To Disk Option to the VM control menu #1359

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/vm/overview/vmOverviewCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ class VmOverviewCard extends React.Component {
<DescriptionListTerm>{_("State")}</DescriptionListTerm>
<DescriptionListDescription>
<StateIcon error={vm.error}
state={vm.state}
state={vm.state + (vm.suspendImage ? " (suspended)" : "")}
valueId={`${idPrefix}-${vm.connectionName}-state`}
dismissError={() => store.dispatch(updateVm({
connectionName: vm.connectionName,
Expand Down
65 changes: 65 additions & 0 deletions src/components/vm/vmActions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import {
domainCanPause,
domainCanShutdown,
domainForceOff,
domainCanSuspendToDisk,
domainCanSuspenImageRemove,
domainForceReboot,
domainInstall,
domainPause,
Expand All @@ -59,6 +61,9 @@ import {
domainShutdown,
domainStart,
domainAddTPM,
domainSuspendToDisk,
domainSuspendImageRemove,
domainHasSuspendImage
} from '../../libvirtApi/domain.js';
import store from "../../store.js";

Expand Down Expand Up @@ -183,6 +188,43 @@ const onSendNMI = (vm) => domainSendNMI({ name: vm.name, id: vm.id, connectionNa
);
});

const onSuspendToDisk = (vm) => domainSuspendToDisk({ name: vm.name, id: vm.id, connectionName: vm.connectionName, flags: vm.state == 'running' ? 2 : 4 }).catch(ex => {
store.dispatch(
updateVm({
connectionName: vm.connectionName,
name: vm.name,
error: {
text: cockpit.format(_("VM $0 failed to save "), vm.name),
detail: ex.message,
}
})
);
});

const onSuspendImageRemove = (vm) => domainSuspendImageRemove({ name: vm.name, id: vm.id, connectionName: vm.connectionName })
.then(() => domainHasSuspendImage({ name: vm.name, id: vm.id, connectionName: vm.connectionName }))
.then((SuspendImage) => {
store.dispatch(
updateVm({
connectionName: vm.connectionName,
name: vm.name,
suspendImage: SuspendImage[0],
})
);
})
.catch(ex => {
store.dispatch(
updateVm({
connectionName: vm.connectionName,
name: vm.name,
error: {
text: cockpit.format(_("VM $0 failed to remove save image "), vm.name),
detail: ex.message,
}
})
);
});

const onAddTPM = (vm, onAddErrorNotification) => domainAddTPM({ connectionName: vm.connectionName, vmName: vm.name })
.catch(ex => onAddErrorNotification({
text: cockpit.format(_("Failed to add TPM to VM $0"), vm.name),
Expand All @@ -208,6 +250,7 @@ const VmActions = ({ vm, vms, onAddErrorNotification, isDetailsPage }) => {

const id = `${vmId(vm.name)}-${vm.connectionName}`;
const state = vm.state;
const suspendImage = vm.suspendImage;
const hasInstallPhase = vm.metadata && vm.metadata.hasInstallPhase;
const dropdownItems = [];

Expand Down Expand Up @@ -235,6 +278,28 @@ const VmActions = ({ vm, vms, onAddErrorNotification, isDetailsPage }) => {
dropdownItems.push(<Divider key="separator-resume" />);
}

if (domainCanSuspendToDisk(state)) {
dropdownItems.push(
<DropdownItem key={`${id}-save`}
id={`${id}-save`}
onClick={() => onSuspendToDisk(vm)}>
{_("Suspend to disk")}
</DropdownItem>
);
dropdownItems.push(<Divider key="separator-suspend" />);
}

if (domainCanSuspenImageRemove(state, suspendImage)) {
dropdownItems.push(
<DropdownItem key={`${id}-saveRemove`}
id={`${id}-savedRemove`}
onClick={() => onSuspendImageRemove(vm)}>
{_("Remove Suspend Image")}
</DropdownItem>
);
dropdownItems.push(<Divider key="separator-suspend" />);
}

if (domainCanShutdown(state)) {
shutdown = (
<Button key='action-shutdown'
Expand Down
2 changes: 2 additions & 0 deletions src/components/vms/hostvmslist.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ const VmState = ({ vm, vms, dismissError }) => {
state = cockpit.format(_("Downloading: $0%"), vm.downloadProgress);
} else if (vm.createInProgress) {
state = _("Creating VM");
} else if (vm.state == "shut off" && vm.suspendImage === true) {
state = `${vm.state} (suspended)`;
} else {
state = vm.state;
}
Expand Down
1 change: 1 addition & 0 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ const transform = {
paused: _("Paused"),
shutdown: _("Shutting down"),
'shut off': _("Shut off"),
'shut off (suspended)': _("Shut off (Suspended)"),
crashed: _("Crashed"),
dying: _("Dying"),
pmsuspended: _("Suspended (PM)"),
Expand Down
27 changes: 27 additions & 0 deletions src/libvirtApi/domain.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ export const domainCanShutdown = (vmState) => domainCanReset(vmState);
export const domainCanPause = (vmState) => vmState == 'running';
export const domainCanRename = (vmState) => vmState == 'shut off';
export const domainCanResume = (vmState) => vmState == 'paused';
export const domainCanSuspendToDisk = (vmState) => vmState == 'running' || vmState == 'paused';
export const domainCanSuspenImageRemove = (vmState, saveImage) => vmState == "shut off" && saveImage == true;
export const domainIsRunning = (vmState) => domainCanReset(vmState);
export const domainSerialConsoleCommand = ({ vm, alias }) => {
if (vm.displays.find(display => display.type == 'pty'))
Expand Down Expand Up @@ -663,6 +665,9 @@ export async function domainGet({
supportsTPM: getDomainCapSupportsTPM(domCaps),
};

const [suspendImage] = await call(connectionName, objPath, 'org.libvirt.Domain', 'HasManagedSaveImage', [0], { timeout, type: 'u' });
props.suspendImage = suspendImage;

const [state] = await call(connectionName, objPath, 'org.libvirt.Domain', 'GetState', [0], { timeout, type: 'u' });
const stateStr = DOMAINSTATE[state[0]];

Expand Down Expand Up @@ -874,6 +879,28 @@ export function domainResume({
return call(connectionName, objPath, 'org.libvirt.Domain', 'Resume', [], { timeout, type: '' });
}

export function domainSuspendToDisk({
connectionName,
id: objPath,
flags
}) {
return call(connectionName, objPath, 'org.libvirt.Domain', 'ManagedSave', [flags], { timeout, type: 'u' });
}

export function domainSuspendImageRemove({
connectionName,
id: objPath,
}) {
return call(connectionName, objPath, 'org.libvirt.Domain', 'ManagedSaveRemove', [0], { timeout, type: 'u' });
}

export function domainHasSuspendImage({
connectionName,
id: objPath,
}) {
return call(connectionName, objPath, 'org.libvirt.Domain', 'HasManagedSaveImage', [0], { timeout, type: 'u' });
}

export function domainSendKey({ connectionName, id, keyCodes }) {
const holdTime = 0;
const flags = 0;
Expand Down