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

vm: Support for VM descriptions #1644

Merged
merged 1 commit into from
Sep 6, 2024
Merged
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
15 changes: 14 additions & 1 deletion src/components/vm/vmActions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { ConfirmDialog } from './confirmDialog.jsx';
import { DeleteDialog } from "./deleteDialog.jsx";
import { MigrateDialog } from './vmMigrateDialog.jsx';
import { RenameDialog } from './vmRenameDialog.jsx';
import { EditDescriptionDialog } from './vmEditDescriptionDialog.jsx';
import { ReplaceSpiceDialog } from './vmReplaceSpiceDialog.jsx';
import {
domainCanDelete,
Expand Down Expand Up @@ -451,9 +452,21 @@ const VmActions = ({ vm, vms, onAddErrorNotification, isDetailsPage }) => {
{_("Rename")}
</DropdownItem>
);
dropdownItems.push(<Divider key="separator-rename" />);
}

if (isDetailsPage) {
dropdownItems.push(
<DropdownItem key={`${id}-edit-description`}
id={`${id}-edit-description`}
onClick={() => Dialogs.show(<EditDescriptionDialog vm={vm} />)}>
{_("Edit description")}
</DropdownItem>
);
}

if (domainCanRename(state) || isDetailsPage)
dropdownItems.push(<Divider key="separator-rename" />);

if (state !== undefined && domainCanDelete(state, vm.id)) {
if (!vm.persistent) {
dropdownItems.push(
Expand Down
6 changes: 6 additions & 0 deletions src/components/vm/vmDetailsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ export const VmDetailsPage = ({
<VmNeedsShutdown vm={vm} />
<VmUsesSpice vm={vm} />
</div>
{
vm.inactiveXML.description &&
<div className="vm-description">
{vm.inactiveXML.description.split("\n").map((p, i) => <p key={i}>{p}</p>)}
</div>
}
</PageSection>
);

Expand Down
82 changes: 82 additions & 0 deletions src/components/vm/vmEditDescriptionDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2024 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/

import cockpit from 'cockpit';
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";
import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea";

import { ModalError } from 'cockpit-components-inline-notification.jsx';
import { useDialogs } from 'dialogs.jsx';

import { isObjectEmpty } from '../../helpers.js';
import { domainSetDescription } from '../../libvirtApi/domain.js';

const _ = cockpit.gettext;

export const EditDescriptionDialog = ({ vm }) => {
const Dialogs = useDialogs();
const [description, setDescription] = useState(vm.inactiveXML.description || "");
const [error, dialogErrorSet] = useState({});

async function onSubmit() {
try {
await domainSetDescription(vm, description);
Dialogs.close();
} catch (exc) {
dialogErrorSet({
dialogError: cockpit.format(_("Failed to set description of VM $0"), vm.name),
dialogErrorDetail: exc.message
Comment on lines +44 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 4 added lines are not executed by any test.

});
}
}

return (
<Modal position="top" variant="small" isOpen onClose={Dialogs.close}
title={cockpit.format(_("Edit description of VM $0"), vm.name)}
footer={
<>
<Button variant='primary'
id="edit-description-dialog-confirm"
onClick={onSubmit}>
{_("Save")}
</Button>
<Button variant='link' onClick={Dialogs.close}>
{_("Cancel")}
</Button>
</>
}>
<Form onSubmit={e => {
e.preventDefault();
onSubmit();
Comment on lines +67 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 3 added lines are not executed by any test.

}}
isHorizontal>
{!isObjectEmpty(error) && <ModalError dialogError={error.dialogError} dialogErrorDetail={error.dialogErrorDetail} />}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

<FormGroup label={_("Description")}
fieldId="edit-description-dialog-description">
<TextArea id='edit-description-dialog-description'
value={description}
onChange={(_, value) => setDescription(value)} />
</FormGroup>
</Form>
</Modal>
);
};
4 changes: 4 additions & 0 deletions src/libvirt-xml-parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ export function parseDomainDumpxml(connectionName, domXml, objPath) {
const metadataElem = getSingleOptionalElem(domainElem, "metadata");

const name = domainElem.getElementsByTagName("name")[0].childNodes[0].nodeValue;
const uuid = domainElem.getElementsByTagName("uuid")[0].childNodes[0].nodeValue;
const description = domainElem.getElementsByTagName("description")[0]?.childNodes[0]?.nodeValue;
const id = objPath;
const osType = osTypeElem.childNodes[0].nodeValue;
const osBoot = parseDumpxmlForOsBoot(osBootElems);
Expand Down Expand Up @@ -292,7 +294,9 @@ export function parseDomainDumpxml(connectionName, domXml, objPath) {

return {
connectionName,
uuid,
name,
description,
id,
osType,
osBoot,
Expand Down
27 changes: 27 additions & 0 deletions src/libvirtApi/domain.js
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,33 @@ export function domainSendNMI({
return call(connectionName, objPath, 'org.libvirt.Domain', 'InjectNMI', [0], { timeout, type: 'u' });
}

function shlex_quote(str) {
// yay, command line apis...
return "'" + str.replaceAll("'", "'\"'\"'") + "'";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

@mvollmer mvollmer Sep 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, what about it? :-) This is supposed to be a JavaScript implementation of it. Is something missing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mistaken this for a Python file, don't ask :)

}

async function domainSetXML(vm, option, values) {
const opts = { err: "message" };
if (vm.connectionName === 'system')
opts.superuser = 'try';

// We don't pass the arguments for virt-xml through a shell, but
// virt-xml does its own parsing with the Python shlex module. So
// we need to do the equivalent of shlex.quote here.

const args = [];
for (const key in values)
args.push(shlex_quote(key + '=' + values[key]));

await cockpit.spawn([
'virt-xml', '-c', `qemu:///${vm.connectionName}`, '--' + option, args.join(','), vm.uuid, '--edit'
], opts);
}

export async function domainSetDescription(vm, description) {
await domainSetXML(vm, "metadata", { description });
}

export function domainSetCpuMode({
name,
connectionName,
Expand Down
21 changes: 21 additions & 0 deletions test/check-machines-lifecycle
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,27 @@ class TestMachinesLifecycle(machineslib.VirtualMachinesCase):
b.wait_text("h2.vm-name", "test%")
b.wait_not_present("#navbar-oops")

def testEditDescription(self):
b = self.browser

self.createVm("mac", running=False)
self.login_and_go("/machines")
self.waitPageInit()
self.goToVmPage("mac")

self.performAction("mac", "edit-description")

# Non-ascii chars, unbalanced quotes, backslash, multiple lines
desc1 = '"Döscrü\\ptiän \'\'\' كرة القدم'
desc2 = 'Second <b>line</b>'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope this does not get rendered as such :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It gets rendered as text, not as HTML.

image


b.set_input_text("#edit-description-dialog-description", desc1 + "\n" + desc2)
b.click("#edit-description-dialog-confirm")
b.wait_not_present("#edit-description-dialog-confirm")

b.wait_text(".vm-description p:nth-child(1)", desc1)
b.wait_text(".vm-description p:nth-child(2)", desc2)

def testDelete(self):
b = self.browser
m = self.machine
Expand Down