diff --git a/src/components/vm/snapshots/vmSnapshotsCreateModal.jsx b/src/components/vm/snapshots/vmSnapshotsCreateModal.jsx
index 8214c3aac..1b47fb3ff 100644
--- a/src/components/vm/snapshots/vmSnapshotsCreateModal.jsx
+++ b/src/components/vm/snapshots/vmSnapshotsCreateModal.jsx
@@ -17,7 +17,7 @@
* along with Cockpit; If not, see .
*/
import cockpit from "cockpit";
-import React from "react";
+import React, { useState } from 'react';
import { Button } from "@patternfly/react-core/dist/esm/components/Button";
import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form";
@@ -30,7 +30,11 @@ 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";
+import {
+ getSortedBootOrderDevices, LIBVIRT_SYSTEM_CONNECTION,
+ units, convertToBestUnit, formatWithBestUnit,
+ dirname
+} from "../../../helpers.js";
import { domainGet } from '../../../libvirtApi/domain.js';
const _ = cockpit.gettext;
@@ -64,10 +68,17 @@ const DescriptionRow = ({ onValueChanged, description }) => {
);
};
-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
+function getDefaultMemoryLocation(vm) {
+ // If we find an existing external snapshot, use it's memory path
+ // as the default. Otherwise, try to find the primary disk and use
+ // it's location. If that fails as well, use a reasonable hard
+ // coded value.
+
+ for (const s of vm.snapshots.sort((a, b) => b.creationTime - a.creationTime)) {
+ if (s.memoryPath)
+ return dirname(s.memoryPath);
+ }
+
const devices = getSortedBootOrderDevices(vm).filter(d => d.bootOrder &&
d.device.device === "disk" &&
d.device.type === "file" &&
@@ -75,30 +86,49 @@ function getDefaultMemoryPath(vm, snapName) {
if (devices.length > 0) {
const primaryDiskPath = devices[0].device.source.file;
const directory = primaryDiskPath.substring(0, primaryDiskPath.lastIndexOf("/") + 1);
- return directory + snapName;
+ return directory;
} else {
if (vm.connectionName === LIBVIRT_SYSTEM_CONNECTION)
- return "/var/lib/libvirt/memory/" + snapName;
+ return "/var/lib/libvirt/memory/";
else if (current_user)
- return current_user.home + "/.local/share/libvirt/memory/" + snapName;
+ return current_user.home + "/.local/share/libvirt/memory/";
}
return "";
}
-const MemoryPathRow = ({ onValueChanged, memoryPath, validationError }) => {
+const MemoryLocationRow = ({ onValueChanged, memoryLocation, validationError, available, needed }) => {
return (
-
+
onValueChanged("memoryPath", value)}
- superuser="try"
- isOptionCreatable
- value={memoryPath} />
-
+ onChange={value => onValueChanged("memoryLocation", value)}
+ value={memoryLocation} />
+
);
};
+function get_available_space(path, callback) {
+ if (!path)
+ callback(null);
+
+ cockpit.spawn(["stat", "-f", "-c", '{ "unit": %S, "free": %a }', path], { superuser: "try" })
+ .then(output => {
+ const info = JSON.parse(output);
+ callback(info.free * info.unit);
+ })
+ .catch(exc => {
+ // channel has already logged the error
+ callback(null);
+ });
+}
+
export class CreateSnapshotModal extends React.Component {
static contextType = DialogsContext;
@@ -107,11 +137,12 @@ export class CreateSnapshotModal extends React.Component {
// cut off seconds, subseconds, and timezone
const now = new Date().toISOString()
.replace(/:[^:]*$/, '');
- const snapName = props.vm.name + '_' + now;
+ const snapName = now;
this.state = {
name: snapName,
description: "",
- memoryPath: getDefaultMemoryPath(props.vm, snapName),
+ memoryLocation: getDefaultMemoryLocation(props.vm),
+ available: null,
inProgress: false,
};
@@ -121,8 +152,18 @@ export class CreateSnapshotModal extends React.Component {
this.onCreate = this.onCreate.bind(this);
}
+ updateAvailableSpace(path) {
+ get_available_space(path, val => this.setState({ available: val }));
+ }
+
onValueChanged(key, value) {
this.setState({ [key]: value });
+ if (key == "memoryLocation") {
+ // We don't need to debounce this. The "memoryLocation"
+ // state is not changed on each keypress, but only when
+ // the input is blurred.
+ this.updateAvailableSpace(value);
+ }
}
dialogErrorSet(text, detail) {
@@ -130,7 +171,7 @@ export class CreateSnapshotModal extends React.Component {
}
onValidate() {
- const { name, memoryPath } = this.state;
+ const { name, memoryLocation } = this.state;
const { vm, isExternal } = this.props;
const validationError = {};
@@ -139,8 +180,8 @@ export class CreateSnapshotModal extends React.Component {
else if (!name)
validationError.name = _("Name can not be empty");
- if (isExternal && vm.state === "running" && !memoryPath)
- validationError.memory = _("Memory file can not be empty");
+ if (isExternal && vm.state === "running" && !memoryLocation)
+ validationError.memory = _("Memory save location can not be empty");
return validationError;
}
@@ -148,17 +189,24 @@ export class CreateSnapshotModal extends React.Component {
onCreate() {
const Dialogs = this.context;
const { vm, isExternal } = this.props;
- const { name, description, memoryPath } = this.state;
+ const { name, description, memoryLocation } = this.state;
const validationError = this.onValidate();
if (!Object.keys(validationError).length) {
this.setState({ inProgress: true });
+ let mpath = null;
+ if (isExternal && vm.state === "running" && memoryLocation) {
+ mpath = memoryLocation;
+ if (mpath[mpath.length-1] != "/")
+ mpath = mpath + "/";
+ mpath = mpath + vm.name + "." + name + ".save";
+ }
snapshotCreate({
vm,
name,
description,
isExternal,
- memoryPath: isExternal && vm.state === "running" && memoryPath,
+ memoryPath: mpath,
})
.then(() => {
// VM Snapshots do not trigger any events so we have to refresh them manually
@@ -175,10 +223,14 @@ export class CreateSnapshotModal extends React.Component {
}
}
+ componentDidMount() {
+ this.updateAvailableSpace(this.state.memoryLocation);
+ }
+
render() {
const Dialogs = this.context;
const { idPrefix, isExternal, vm } = this.props;
- const { name, description, memoryPath } = this.state;
+ const { name, description, memoryLocation, available } = this.state;
const validationError = this.onValidate();
const body = (
@@ -186,8 +238,10 @@ export class CreateSnapshotModal extends React.Component {
{isExternal && vm.state === 'running' &&
- }
+ }
);
diff --git a/src/helpers.js b/src/helpers.js
index efe73696c..e50be3e97 100644
--- a/src/helpers.js
+++ b/src/helpers.js
@@ -145,6 +145,12 @@ export function convertToUnitVerbose(input, inputUnit, outputUnit) {
return result;
}
+export function formatWithBestUnit(input, inputUnit) {
+ const best = convertToBestUnit(input, inputUnit || units.B);
+ const decimals = best.value >= 100 ? 0 : best.value >= 10 ? 1 : 2;
+ return cockpit.format("$0 $1", best.value.toFixed(decimals), best.unit);
+}
+
export function isEmpty(str) {
return (!str || str.length === 0);
}
@@ -907,3 +913,11 @@ export function vmSupportsExternalSnapshots(config, vm, storagePools) {
return true;
}
+
+export function dirname(path) {
+ const i = path.lastIndexOf("/");
+ if (i < 0)
+ return null;
+ else
+ return path.substr(0, i);
+}
diff --git a/src/libvirt-xml-parse.js b/src/libvirt-xml-parse.js
index 74492abc0..4da8bd7a1 100644
--- a/src/libvirt-xml-parse.js
+++ b/src/libvirt-xml-parse.js
@@ -216,14 +216,16 @@ export function parseDomainSnapshotDumpxml(snapshot) {
const nameElem = getSingleOptionalElem(snapElem, 'name');
const descElem = getSingleOptionalElem(snapElem, 'description');
const parentElem = getSingleOptionalElem(snapElem, 'parent');
+ const memElem = getSingleOptionalElem(snapElem, 'memory');
const name = nameElem?.childNodes[0].nodeValue;
const description = descElem?.childNodes[0].nodeValue;
const parentName = parentElem?.getElementsByTagName("name")[0].childNodes[0].nodeValue;
const state = snapElem.getElementsByTagName("state")[0].childNodes[0].nodeValue;
const creationTime = snapElem.getElementsByTagName("creationTime")[0].childNodes[0].nodeValue;
+ const memoryPath = memElem?.getAttribute("file");
- return { name, description, state, creationTime, parentName };
+ return { name, description, state, creationTime, parentName, memoryPath };
}
export function parseDomainDumpxml(connectionName, domXml, objPath) {