diff --git a/src/components/vm/snapshots/vmSnapshotsCard.jsx b/src/components/vm/snapshots/vmSnapshotsCard.jsx
index a7e2dcd94..24217f884 100644
--- a/src/components/vm/snapshots/vmSnapshotsCard.jsx
+++ b/src/components/vm/snapshots/vmSnapshotsCard.jsx
@@ -20,7 +20,7 @@ import React from 'react';
import cockpit from 'cockpit';
import { useDialogs, DialogsContext } from 'dialogs.jsx';
-import { vmId, localize_datetime } from "../../../helpers.js";
+import { vmId, localize_datetime, vmSupportsExternalSnapshots } from "../../../helpers.js";
import { CreateSnapshotModal } from "./vmSnapshotsCreateModal.jsx";
import { ListingTable } from "cockpit-components-table.jsx";
import { Button } from "@patternfly/react-core/dist/esm/components/Button";
@@ -35,12 +35,16 @@ import './vmSnapshotsCard.scss';
const _ = cockpit.gettext;
-export const VmSnapshotsActions = ({ vm }) => {
+export const VmSnapshotsActions = ({ vm, config, storagePools }) => {
const Dialogs = useDialogs();
const id = vmId(vm.name);
+ const isExternal = vmSupportsExternalSnapshots(config, vm, storagePools);
+
function open() {
Dialogs.show();
}
diff --git a/src/components/vm/snapshots/vmSnapshotsCreateModal.jsx b/src/components/vm/snapshots/vmSnapshotsCreateModal.jsx
index 31455f56e..664e4f75e 100644
--- a/src/components/vm/snapshots/vmSnapshotsCreateModal.jsx
+++ b/src/components/vm/snapshots/vmSnapshotsCreateModal.jsx
@@ -28,10 +28,15 @@ import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput"
import { FormHelper } from 'cockpit-components-form-helper.jsx';
import { DialogsContext } from 'dialogs.jsx';
import { ModalError } from "cockpit-components-inline-notification.jsx";
+import { FileAutoComplete } from "cockpit-components-file-autocomplete.jsx";
import { snapshotCreate, snapshotGetAll } from "../../../libvirtApi/snapshot.js";
+import { getSortedBootOrderDevices, LIBVIRT_SYSTEM_CONNECTION } from "../../../helpers.js";
const _ = cockpit.gettext;
+let current_user = null;
+cockpit.user().then(user => { current_user = user });
+
const NameRow = ({ onValueChanged, name, validationError }) => {
return (
{
);
};
+function getDefaultMemoryPath(vm, snapName) {
+ // Choosing a default path where memory snapshot should be stored might be tricky. Ideally we want
+ // to store it in the same directory where the primary disk (the disk which is first booted) is stored
+ // If howver no such disk can be found, we should fallback to libvirt's default /var/lib/libvirt
+ const devices = getSortedBootOrderDevices(vm).filter(d => d.bootOrder &&
+ d.device.device === "disk" &&
+ d.device.type === "file" &&
+ d.device.source.file);
+ if (devices.length > 0) {
+ const primaryDiskPath = devices[0].device.source.file;
+ const directory = primaryDiskPath.substring(0, primaryDiskPath.lastIndexOf("/") + 1);
+ return directory + snapName;
+ } else {
+ if (vm.connectionName === LIBVIRT_SYSTEM_CONNECTION)
+ return "/var/lib/libvirt/memory/" + snapName;
+ else if (current_user)
+ return current_user.home + "/.local/share/libvirt/memory/" + snapName;
+ }
+
+ return "";
+}
+
+const MemoryPathRow = ({ onValueChanged, memoryPath }) => {
+ return (
+
+ onValueChanged("memoryPath", value)}
+ superuser="try"
+ value={memoryPath} />
+
+ );
+};
+
export class CreateSnapshotModal extends React.Component {
static contextType = DialogsContext;
@@ -66,9 +104,11 @@ export class CreateSnapshotModal extends React.Component {
// cut off seconds, subseconds, and timezone
const now = new Date().toISOString()
.replace(/:[^:]*$/, '');
+ const snapName = props.vm.name + '_' + now;
this.state = {
- name: props.vm.name + '_' + now,
+ name: snapName,
description: "",
+ memoryPath: getDefaultMemoryPath(props.vm, snapName),
inProgress: false,
};
@@ -87,8 +127,8 @@ export class CreateSnapshotModal extends React.Component {
}
onValidate(submitted = false) {
- const { name } = this.state;
- const { vm } = this.props;
+ const { name, memoryPath } = this.state;
+ const { vm, isExternal } = this.props;
const validationError = {};
if (vm.snapshots.findIndex(snap => snap.name === name) > -1)
@@ -96,20 +136,33 @@ export class CreateSnapshotModal extends React.Component {
else if (!name && submitted)
validationError.name = _("Name should not be empty");
+ if (isExternal && vm.state === "running" && !memoryPath)
+ validationError.name = _("Memory file should not be empty");
+
return validationError;
}
onCreate() {
const Dialogs = this.context;
- const { vm } = this.props;
- const { name, description } = this.state;
+ const { vm, isExternal, storagePools } = this.props;
+ const { name, description, memoryPath } = this.state;
+ const disks = Object.values(vm.disks);
const validationError = this.onValidate(true);
this.setState({ submitted: true });
if (!Object.keys(validationError).length) {
this.setState({ inProgress: true });
- snapshotCreate({ connectionName: vm.connectionName, vmId: vm.id, name, description })
+ snapshotCreate({
+ connectionName: vm.connectionName,
+ vmId: vm.id,
+ name,
+ description,
+ memoryPath: vm.state === "running" && memoryPath,
+ disks,
+ isExternal,
+ storagePools
+ })
.then(() => {
// VM Snapshots do not trigger any events so we have to refresh them manually
snapshotGetAll({ connectionName: vm.connectionName, domainPath: vm.id });
@@ -124,14 +177,15 @@ export class CreateSnapshotModal extends React.Component {
render() {
const Dialogs = this.context;
- const { idPrefix } = this.props;
- const { name, description, submitted } = this.state;
+ const { idPrefix, isExternal, vm } = this.props;
+ const { name, description, memoryPath, submitted } = this.state;
const validationError = this.onValidate(submitted);
const body = (
);
diff --git a/src/components/vm/vmDetailsPage.jsx b/src/components/vm/vmDetailsPage.jsx
index b1dea90ca..62ed332fd 100644
--- a/src/components/vm/vmDetailsPage.jsx
+++ b/src/components/vm/vmDetailsPage.jsx
@@ -173,7 +173,7 @@ export const VmDetailsPage = ({
id: cockpit.format("$0-snapshots", vmId(vm.name)),
className: "snapshots-card",
title: _("Snapshots"),
- actions: ,
+ actions: ,
body:
});
}
diff --git a/src/libvirt-xml-create.js b/src/libvirt-xml-create.js
index d1747c583..bdcba0293 100644
--- a/src/libvirt-xml-create.js
+++ b/src/libvirt-xml-create.js
@@ -1,3 +1,5 @@
+import { getStoragePoolPath } from "./helpers.js";
+
export function getDiskXML(type, file, device, poolName, volumeName, format, target, cacheMode, shareable, busType, serial) {
const doc = document.implementation.createDocument('', '', null);
@@ -216,7 +218,8 @@ export function getPoolXML({ name, type, source, target }) {
return new XMLSerializer().serializeToString(doc.documentElement);
}
-export function getSnapshotXML(name, description) {
+// see https://libvirt.org/formatsnapshot.html
+export function getSnapshotXML(name, description, disks, memoryPath, isExternal, storagePools, connectionName) {
const doc = document.implementation.createDocument('', '', null);
const snapElem = doc.createElement('domainsnapshot');
@@ -233,6 +236,39 @@ export function getSnapshotXML(name, description) {
snapElem.appendChild(descriptionElem);
}
+ if (isExternal) {
+ if (memoryPath) {
+ const memoryElem = doc.createElement('memory');
+ memoryElem.setAttribute('snapshot', 'external');
+ memoryElem.setAttribute('file', memoryPath);
+ snapElem.appendChild(memoryElem);
+ }
+
+ const disksElem = doc.createElement('disks');
+ disks.forEach(disk => {
+ // Disk can have attribute "snapshot" set to "no", which means no snapshot should be created of the said disk
+ // This cannot be configured through cockpit, but we should uphold it nevertheless
+ // see "snapshot" attribute of element at https://libvirt.org/formatdomain.html#hard-drives-floppy-disks-cdroms
+ if (disk.snapshot !== "no") {
+ const diskElem = doc.createElement('disk');
+ diskElem.setAttribute('name', disk.target);
+ diskElem.setAttribute('snapshot', 'external');
+
+ if (disk.type === "volume") {
+ const poolPath = getStoragePoolPath(storagePools, disk.source.pool, connectionName);
+ if (poolPath) {
+ const sourceElem = doc.createElement('source');
+ sourceElem.setAttribute('file', `${poolPath}/${disk.source.volume}.snap`);
+ diskElem.appendChild(sourceElem);
+ }
+ }
+
+ disksElem.appendChild(diskElem);
+ }
+ });
+ snapElem.appendChild(disksElem);
+ }
+
doc.appendChild(snapElem);
return new XMLSerializer().serializeToString(doc.documentElement);
diff --git a/src/libvirt-xml-parse.js b/src/libvirt-xml-parse.js
index 14ab7af77..590722388 100644
--- a/src/libvirt-xml-parse.js
+++ b/src/libvirt-xml-parse.js
@@ -451,6 +451,7 @@ export function parseDumpxmlForDisks(devicesElem) {
},
bootOrder: bootElem?.getAttribute('order'),
type: diskElem.getAttribute('type'), // i.e.: file
+ snapshot: diskElem.getAttribute('snapshot'), // i.e.: internal, external
device: diskElem.getAttribute('device'), // i.e. cdrom, disk
source: {
file: sourceElem?.getAttribute('file'), // optional file name of the disk
diff --git a/src/libvirtApi/snapshot.js b/src/libvirtApi/snapshot.js
index 51bca60de..9bbc33048 100644
--- a/src/libvirtApi/snapshot.js
+++ b/src/libvirtApi/snapshot.js
@@ -29,9 +29,8 @@ import { parseDomainSnapshotDumpxml } from '../libvirt-xml-parse.js';
import { call, Enum, timeout } from './helpers.js';
import { logDebug } from '../helpers.js';
-export function snapshotCreate({ connectionName, vmId, name, description }) {
- const xmlDesc = getSnapshotXML(name, description);
-
+export function snapshotCreate({ connectionName, vmId, name, description, memoryPath, disks, isExternal, storagePools }) {
+ const xmlDesc = getSnapshotXML(name, description, disks, memoryPath, isExternal, storagePools, connectionName);
return call(connectionName, vmId, 'org.libvirt.Domain', 'SnapshotCreateXML', [xmlDesc, 0], { timeout, type: 'su' });
}