Skip to content

Commit

Permalink
Vsock interface can be configured
Browse files Browse the repository at this point in the history
The vsock facilitates communication between virtual machines and the
host they are running on independent of virtual machine network
configuration.
  • Loading branch information
skobyda committed May 11, 2023
1 parent 61867af commit db9e532
Show file tree
Hide file tree
Showing 7 changed files with 517 additions and 2 deletions.
31 changes: 30 additions & 1 deletion src/components/vm/overview/vmOverviewCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import cockpit from 'cockpit';
import { Button } from "@patternfly/react-core/dist/esm/components/Button";
import { Icon } from "@patternfly/react-core/dist/esm/components/Icon";
import { Text, TextVariants } from "@patternfly/react-core/dist/esm/components/Text";
import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip";
import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList";
import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex";
import { Popover } from "@patternfly/react-core/dist/esm/components/Popover";
import { Switch } from "@patternfly/react-core/dist/esm/components/Switch";
import { DialogsContext } from 'dialogs.jsx';
import { HelpIcon } from '@patternfly/react-icons';
Expand All @@ -41,6 +43,7 @@ import { BootOrderLink } from './bootOrder.jsx';
import { FirmwareLink } from './firmware.jsx';
import { WatchdogLink } from './watchdog.jsx';
import { needsShutdownCpuModel, NeedsShutdownTooltip, needsShutdownVcpu } from '../../common/needsShutdown.jsx';
import { VsockLink } from './vsock.jsx';
import { StateIcon } from '../../common/stateIcon.jsx';
import { domainChangeAutostart, domainGet } from '../../../libvirtApi/domain.js';
import store from '../../../store.js';
Expand All @@ -50,6 +53,7 @@ import '../../common/overviewCard.css';
const _ = cockpit.gettext;

const WATCHDOG_INFO_MESSAGE = _("Watchdogs act when systems stop responding. To use this virtual watchdog device, the guest system also needs to have an additional driver and a running watchdog service.");
const VSOCK_INFO_MESSAGE = _("Virtual socket support enables communication between the host and guest over a socket. It still requires special vsock-aware software to communicate over the socket.");

class VmOverviewCard extends React.Component {
static contextType = DialogsContext;
Expand Down Expand Up @@ -93,7 +97,7 @@ class VmOverviewCard extends React.Component {
}

render() {
const { vm, nodeDevices, libvirtVersion } = this.props;
const { vm, vms, nodeDevices, libvirtVersion } = this.props;
const idPrefix = vmId(vm.name);

const autostart = (
Expand Down Expand Up @@ -217,6 +221,30 @@ class VmOverviewCard extends React.Component {
<WatchdogLink vm={vm} idPrefix={idPrefix} infoMessage={WATCHDOG_INFO_MESSAGE} />
</DescriptionListDescription>
</DescriptionListGroup>

<DescriptionListGroup>
<Flex spaceItems={{ default: 'spaceItemsXs' }} alignItems={{ default: 'alignItemsCenter' }}>
<FlexItem>
<DescriptionListTerm>
{_("Vsock")}
</DescriptionListTerm>
</FlexItem>
<FlexItem>
<Popover alertSeverityVariant="info"
position="right"
bodyContent={VSOCK_INFO_MESSAGE}>
<button onClick={e => e.preventDefault()} className="pf-c-form__group-label-help">
<Icon className="overview-icon" status="info">
<HelpIcon noVerticalAlign />
</Icon>
</button>
</Popover>
</FlexItem>
</Flex>
<DescriptionListDescription id={`${idPrefix}-vsock`}>
<VsockLink vm={vm} vms={vms} idPrefix={idPrefix} infoMessage={VSOCK_INFO_MESSAGE} />
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
</FlexItem>
<FlexItem>
Expand Down Expand Up @@ -247,6 +275,7 @@ class VmOverviewCard extends React.Component {

VmOverviewCard.propTypes = {
vm: PropTypes.object.isRequired,
vms: PropTypes.array.isRequired,
config: PropTypes.object.isRequired,
libvirtVersion: PropTypes.number.isRequired,
nodeDevices: PropTypes.array.isRequired,
Expand Down
258 changes: 258 additions & 0 deletions src/components/vm/overview/vsock.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2023 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 React, { useState } from 'react';
import PropTypes from 'prop-types';
import cockpit from 'cockpit';
import { Button } from "@patternfly/react-core/dist/esm/components/Button";
import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex";
import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form";
import { NumberInput } from "@patternfly/react-core/dist/esm/components/NumberInput";
import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox";
import { useDialogs } from 'dialogs.jsx';
import { fmt_to_fragments } from 'utils.jsx';

import { ModalError } from 'cockpit-components-inline-notification.jsx';
import { FormHelper } from "cockpit-components-form-helper.jsx";
import { domainRemoveVsock, domainSetVsock } from "../../../libvirtApi/domain.js";
import { NeedsShutdownAlert, NeedsShutdownTooltip } from "../../common/needsShutdown.jsx";

import "./vsock.scss";

const _ = cockpit.gettext;

const ASSIGN_AUTOMATICALLY = _("Assign automatically");
// There are several reserved addresses:
// VMADDR_CID_ANY (-1U) means any address for binding;
// VMADDR_CID_HYPERVISOR (0) is reserved for services built into the hypervisor;
// VMADDR_CID_LOCAL (1) is the well-known address for local communication (loopback);
// VMADDR_CID_HOST (2) is the well-known address of the host.
const MIN_VSOCK_CID = 3;

function getVsockUsageMessage(vmName, connectionName, vms, auto, address) {
if (auto)
return;

const vmsUsingCIDAddress = vms
.filter(vm => !(vmName === vm.name && connectionName === vm.connectionName) && vm.vsock.cid.auto === "no" && vm.vsock.cid.address === String(address))
.map(vm => vm.name);
if (vmsUsingCIDAddress.length === 0)
return;

const vmsUsingCIDAddressString = vmsUsingCIDAddress.join(", ");

return (
<span>
{fmt_to_fragments(_("Identifier in use by $0. VMs with an identical identifier cannot run at the same time."), <b>{vmsUsingCIDAddressString}</b>)}
</span>
);
}

function getNextAvailableVsockCID(vmName, connectionName, vms) {
let availableAddress = MIN_VSOCK_CID;

const vmsCIDAddresses = vms.filter(vm => !(vmName === vm.name && connectionName === vm.connectionName) && vm.vsock.cid.auto === "no")
.map(vm => Number(vm.vsock.cid.address))
.sort();

for (let i = 0; i < vmsCIDAddresses.length; i++) {
const addressInUse = vmsCIDAddresses[i];
if (availableAddress === addressInUse) {
availableAddress++;
i = 0;
}
}

return availableAddress;
}

export const VsockModal = ({ vm, vms, vmVsockNormalized, isVsockAttached, idPrefix, infoMessage }) => {
const [dialogError, setDialogError] = useState();
const [auto, setAuto] = useState(vm.vsock.cid.auto ? vmVsockNormalized.auto : true);
const [address, _setAddress] = useState(vmVsockNormalized.address || getNextAvailableVsockCID(vm.name, vm.connectionName, vms));
const [actionInProgress, setActionInProgress] = useState(undefined);

const setAddress = (value) => {
// Allow empty string
if (value === "") {
_setAddress(value);
return;
}

_setAddress(parseInt(value));
};

const onBlur = (value) => {
if (value < MIN_VSOCK_CID)
value = MIN_VSOCK_CID;

_setAddress(value);
};

const Dialogs = useDialogs();

const save = () => {
setActionInProgress("save");
return domainSetVsock({
connectionName: vm.connectionName,
vmName: vm.name,
hotplug: vm.state === "running",
permanent: vm.persistent,
auto: auto ? "yes" : "no",
address,
isVsockAttached,
})
.then(Dialogs.close, exc => setDialogError({ text: _("Failed to configure vsock"), detail: exc.message }))
.finally(() => setActionInProgress(undefined));
};

const detach = () => {
setActionInProgress("detach");
return domainRemoveVsock({
connectionName: vm.connectionName,
vmName: vm.name,
hotplug: vm.state === "running",
permanent: vm.persistent,
})
.then(Dialogs.close, exc => setDialogError({ text: _("Failed to detach vsock"), detail: exc.message }))
.finally(() => setActionInProgress(undefined));
};

const showWarning = () => {
if (isVsockAttached && vm.persistent && vm.state === "running" &&
(vmVsockNormalized.auto !== auto ||
// If automatic generation is set, then adress in live XML is prefilled with a value libvirt chooses,
// and it's expected that live XML will contain different address than inactiveXML
(!auto && vmVsockNormalized.address !== address)))
return <NeedsShutdownAlert idPrefix={idPrefix} />;
};

const vsockUsage = getVsockUsageMessage(vm.name, vm.connectionName, vms, auto, address);

const body = (
<Form onSubmit={e => e.preventDefault()} isHorizontal>
<FormGroup fieldId="vsock-cid"
label={_("Custom identifier")}
isInline>
<Flex alignItems={{ default: 'alignItemsCenter' }} spaceItems={{ default: 'spaceItemsSm' }}>
<Checkbox id='vsock-cid-generate'
isChecked={!auto}
onChange={() => setAuto(!auto)} />
<NumberInput value={!auto ? address : ""}
onMinus={() => setAddress(address - 1)}
onChange={event => setAddress(event.target.value)}
onPlus={() => setAddress(address + 1)}
onBlur={event => onBlur(event.target.value)}
min={MIN_VSOCK_CID}
isDisabled={auto}
inputName="vsock-context-identifier"
id="vsock-context-identifier"
inputAriaLabel="vsock context identifier"
allowEmptyInput
widthChars={4} />
</Flex>
<FormHelper fieldId="vsock-cid-usage"
variant="warning"
helperTextInvalid={vsockUsage}
helperText={vsockUsage} />
</FormGroup>
</Form>
);

return (
<Modal id={`${idPrefix}-vsock-modal`}
position="top"
variant="small"
onClose={Dialogs.close}
title={isVsockAttached ? _("Edit vsock interface") : _("Add vsock interface")}
description={infoMessage}
isOpen
footer={
<>
<Button variant='primary'
id="vsock-dialog-apply"
onClick={save}
isLoading={actionInProgress == "save"}
isDisabled={actionInProgress}>
{isVsockAttached ? _("Save") : _("Add")}
</Button>
{isVsockAttached &&
<Button variant='secondary'
id="vsock-dialog-detach"
onClick={detach}
isLoading={actionInProgress == "detach"}
isDisabled={actionInProgress}>
{_("Remove")}
</Button>}
<Button variant='link' onClick={Dialogs.close}>
{_("Cancel")}
</Button>
</>
}>
{showWarning()}
{dialogError && <ModalError dialogError={dialogError.text} dialogErrorDetail={dialogError.detail} />}
{body}
</Modal>
);
};

VsockModal.propTypes = {
vm: PropTypes.object.isRequired,
vmVsockNormalized: PropTypes.object.isRequired,
vms: PropTypes.array.isRequired,
isVsockAttached: PropTypes.bool.isRequired,
idPrefix: PropTypes.string.isRequired,
infoMessage: PropTypes.string.isRequired,
};

export const VsockLink = ({ vm, vms, idPrefix, infoMessage }) => {
const Dialogs = useDialogs();
const isVsockAttached = Object.keys(vm.vsock.cid).length > 0;
const vmVsockNormalized = {
auto: vm.vsock.cid.auto && vm.vsock.cid.auto === "yes",
address: vm.vsock.cid.address && Number(vm.vsock.cid.address),
};
const vsockActionChanged = vm.persistent && vm.state === "running" &&
(vm.inactiveXML.vsock.cid.auto !== vm.vsock.cid.auto ||
// If automatic generation is set, then adress in live XML is prefilled with a value libvirt chooses,
// and it's expected that live XML will contain different address than inactiveXML
(!vmVsockNormalized.auto && vm.inactiveXML.vsock.cid.address !== vm.vsock.cid.address));
let vsockAddress = _("none");
if (vmVsockNormalized.auto && vm.state !== "running") {
vsockAddress = ASSIGN_AUTOMATICALLY.toLowerCase(); // small hack so translators don't have to translate both uppercase and lowercase string
} else if (vmVsockNormalized.address) {
vsockAddress = vmVsockNormalized.address;
}

function open() {
Dialogs.show(<VsockModal vm={vm} vms={vms} vmVsockNormalized={vmVsockNormalized} isVsockAttached={isVsockAttached} idPrefix={idPrefix} infoMessage={infoMessage} />);
}

return (
<Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
<FlexItem id={`${idPrefix}-vsock-address`}>
{vsockAddress}
</FlexItem>
{ vsockActionChanged && <NeedsShutdownTooltip iconId="vsock-tooltip" tooltipId="tip-vsock" /> }
<Button variant="link" isInline id={`${idPrefix}-vsock-button`} onClick={open}>
{isVsockAttached ? _("edit") : _("add")}
</Button>
</Flex>
);
};
6 changes: 6 additions & 0 deletions src/components/vm/overview/vsock.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.ct-input-group-spacer-sm.pf-l-flex {
// Limit width for select entries and inputs in the input groups otherwise they take up the whole space
> .pf-c-select, .pf-c-form-control:not(.pf-c-select__toggle-typeahead) {
max-width: 8ch;
}
}
4 changes: 3 additions & 1 deletion src/components/vm/vmDetailsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ export const VmDetailsPage = ({
{
id: `${vmId(vm.name)}-overview`,
title: _("Overview"),
body: <VmOverviewCard vm={vm} config={config}
body: <VmOverviewCard vm={vm}
vms={vms}
config={config}
loaderElems={vm.capabilities.loaderElems}
maxVcpu={vm.capabilities.maxVcpu}
cpuModels={vm.capabilities.cpuModels}
Expand Down
19 changes: 19 additions & 0 deletions src/libvirt-xml-parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ export function parseDomainDumpxml(connectionName, domXml, objPath) {
const hostDevices = parseDumpxmlForHostDevices(devicesElem);
const filesystems = parseDumpxmlForFilesystems(devicesElem);
const watchdog = parseDumpxmlForWatchdog(devicesElem);
const vsock = parseDumpxmlForVsock(devicesElem);

const hasInstallPhase = parseDumpxmlMachinesMetadataElement(metadataElem, 'has_install_phase') === 'true';
const installSourceType = parseDumpxmlMachinesMetadataElement(metadataElem, 'install_source_type');
Expand Down Expand Up @@ -286,6 +287,7 @@ export function parseDomainDumpxml(connectionName, domXml, objPath) {
hostDevices,
filesystems,
watchdog,
vsock,
metadata,
};
}
Expand Down Expand Up @@ -462,6 +464,23 @@ export function parseDumpxmlForWatchdog(devicesElem) {
}
}

export function parseDumpxmlForVsock(devicesElem) {
const vsockElem = getSingleOptionalElem(devicesElem, 'vsock');
const cid = {};

if (vsockElem) {
const cidElem = getSingleOptionalElem(devicesElem, 'cid');

if (cidElem) {
// https://libvirt.org/formatdomain.html#vsock
cid.auto = cidElem.getAttribute('auto');
cid.address = cidElem.getAttribute('address');
}
}

return { cid };
}

export function parseDumpxmlForFilesystems(devicesElem) {
const filesystems = [];
const filesystemElems = devicesElem.getElementsByTagName('filesystem');
Expand Down
Loading

0 comments on commit db9e532

Please sign in to comment.