Skip to content

Commit

Permalink
Create external snapshots when possible
Browse files Browse the repository at this point in the history
  • Loading branch information
skobyda committed Oct 30, 2023
1 parent ab42eb6 commit a492f2d
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 15 deletions.
8 changes: 6 additions & 2 deletions src/components/vm/snapshots/vmSnapshotsCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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(<CreateSnapshotModal idPrefix={`${id}-create-snapshot`}
isExternal={isExternal}
storagePools={storagePools}
vm={vm} />);
}

Expand Down
70 changes: 62 additions & 8 deletions src/components/vm/snapshots/vmSnapshotsCreateModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<FormGroup
Expand All @@ -58,6 +63,39 @@ 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
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 (
<FormGroup fieldId="snapshot-create-dialog-memory-path" label={_("Memory file")}>
<FileAutoComplete id="snapshot-create-dialog-memory-path"
onChange={value => onValueChanged("memoryPath", value)}
superuser="try"
value={memoryPath} />
</FormGroup>
);
};

export class CreateSnapshotModal extends React.Component {
static contextType = DialogsContext;

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

Expand All @@ -87,29 +127,42 @@ 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)
validationError.name = _("Name already exists");
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 });
Expand All @@ -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 = (
<Form onSubmit={e => e.preventDefault()} isHorizontal>
<NameRow name={name} validationError={validationError} onValueChanged={this.onValueChanged} />
<DescriptionRow description={description} onValueChanged={this.onValueChanged} />
{isExternal && vm.state === 'running' && <MemoryPathRow memoryPath={memoryPath} onValueChanged={this.onValueChanged} />}
</Form>
);

Expand Down
2 changes: 1 addition & 1 deletion src/components/vm/vmDetailsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export const VmDetailsPage = ({
id: cockpit.format("$0-snapshots", vmId(vm.name)),
className: "snapshots-card",
title: _("Snapshots"),
actions: <VmSnapshotsActions vm={vm} />,
actions: <VmSnapshotsActions vm={vm} config={config} storagePools={storagePools} />,
body: <VmSnapshotsCard vm={vm} config={config} />
});
}
Expand Down
38 changes: 37 additions & 1 deletion src/libvirt-xml-create.js
Original file line number Diff line number Diff line change
@@ -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);

Expand Down Expand Up @@ -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');
Expand All @@ -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 <disk> 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);
Expand Down
1 change: 1 addition & 0 deletions src/libvirt-xml-parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions src/libvirtApi/snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
}

Expand Down

0 comments on commit a492f2d

Please sign in to comment.