Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
mvollmer committed Jul 19, 2024
1 parent 1e3ffd5 commit e6d3102
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 27 deletions.
106 changes: 80 additions & 26 deletions src/components/vm/snapshots/vmSnapshotsCreateModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
import cockpit from "cockpit";
import React from "react";
import React, { useState } from 'react';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import useState.

import { Button } from "@patternfly/react-core/dist/esm/components/Button";
import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form";
Expand All @@ -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";

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused imports convertToBestUnit, units.
import { domainGet } from '../../../libvirtApi/domain.js';

const _ = cockpit.gettext;
Expand Down Expand Up @@ -64,41 +68,67 @@ 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" &&
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;
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 (
<FormGroup id="snapshot-create-dialog-memory-path" label={_("Memory file")}>
<FormGroup id="snapshot-create-dialog-memory-location" label={_("Memory save location")}>
<FileAutoComplete
onChange={value => onValueChanged("memoryPath", value)}
superuser="try"
isOptionCreatable
value={memoryPath} />
<FormHelper helperTextInvalid={validationError} />
onChange={value => onValueChanged("memoryLocation", value)}
value={memoryLocation} />
<FormHelper helperTextInvalid={validationError}
helperText={available
? cockpit.format(_("$0 available at this location. About $1 are needed to save the memory state of the machine."),
formatWithBestUnit(available),
formatWithBestUnit(needed))
: cockpit.format(_("About $0 are needed to save the memory state of the machine."),
formatWithBestUnit(needed))} />
</FormGroup>
);
};

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;

Expand All @@ -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,
};

Expand All @@ -121,16 +152,26 @@ 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) {
this.setState({ dialogError: text, dialogErrorDetail: detail });
}

onValidate() {
const { name, memoryPath } = this.state;
const { name, memoryLocation } = this.state;
const { vm, isExternal } = this.props;
const validationError = {};

Expand All @@ -139,26 +180,33 @@ 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;
}

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
Expand All @@ -175,19 +223,25 @@ 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 = (
<Form onSubmit={e => e.preventDefault()} isHorizontal>
<NameRow name={name} validationError={validationError.name} onValueChanged={this.onValueChanged} />
<DescriptionRow description={description} onValueChanged={this.onValueChanged} />
{isExternal && vm.state === 'running' &&
<MemoryPathRow memoryPath={memoryPath} onValueChanged={this.onValueChanged}
validationError={validationError.memory} />}
<MemoryLocationRow memoryLocation={memoryLocation} onValueChanged={this.onValueChanged}
validationError={validationError.memory}
available={available}
needed={vm.rssMemory*1024} />}
</Form>
);

Expand Down
14 changes: 14 additions & 0 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
4 changes: 3 additions & 1 deletion src/libvirt-xml-parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit e6d3102

Please sign in to comment.