* Copyright (C) 2018 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import 'cockpit-dark-theme';
import '../lib/patternfly/patternfly-6-cockpit.scss';
import 'polyfills';
import cockpit from "cockpit";
import React from "react";
import { createRoot } from 'react-dom/client';
import * as timeformat from 'timeformat';
import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
import { DataList, DataListAction, DataListCell, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList/index.js";
import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
import { EmptyState } from "@patternfly/react-core/dist/esm/components/EmptyState/index.js";
import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
import { Gallery } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
import { Page, PageBreadcrumb, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
import { Content, ContentVariants } from "@patternfly/react-core/dist/esm/components/Content/index.js";
import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js";
import {
Modal, ModalBody, ModalFooter, ModalHeader
} from '@patternfly/react-core/dist/esm/components/Modal/index.js';
import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
import { ExternalLinkAltIcon } from "@patternfly/react-icons";
import { SortByDirection } from "@patternfly/react-table";
import { ListingTable } from "cockpit-components-table.jsx";
import { WithDialogs, useDialogs } from "dialogs.jsx";
import kernelopt_sh from "kernelopt.sh";
import detect from "./hw-detect.js";
import { superuser } from "superuser";
import { PrivilegedButton } from "cockpit-components-privileged";
import { useInit } from "hooks";
import "./hwinfo.scss";
const _ = cockpit.gettext;
const SystemInfo = ({ info, onSecurityClick }) => {
if ((!info.name || !info.version) && info.alt_name && info.alt_version) {
info.name = info.alt_name;
info.version = info.alt_version;
}
const mitigations = (
<PrivilegedButton variant="link" buttonId="cpu_mitigations" tooltipId="tip-cpu-security"
excuse={ _("The user $0 is not permitted to change cpu security mitigations") }
onClick={ onSecurityClick }>
{ _("Mitigations") }
</PrivilegedButton>
);
const bios_date = Date.parse(info.bios_date);
return (
<Flex id="hwinfo-system-info-list" direction={{ default: 'column', sm: 'row' }}>
<FlexItem className="hwinfo-system-info-list-item" flex={{ default: 'flex_1' }}>
<DescriptionList className="pf-m-horizontal-on-md">
{ info.type &&
<DescriptionListGroup>
<DescriptionListTerm>{ _("Type") }</DescriptionListTerm>
<DescriptionListDescription>{ info.type }</DescriptionListDescription>
</DescriptionListGroup> }
{ info.name &&
<DescriptionListGroup>
<DescriptionListTerm>{ _("Name") }</DescriptionListTerm>
<DescriptionListDescription>{ info.name }</DescriptionListDescription>
</DescriptionListGroup> }
{ info.version &&
<DescriptionListGroup>
<DescriptionListTerm>{ _("Version") }</DescriptionListTerm>
<DescriptionListDescription>{ info.version }</DescriptionListDescription>
</DescriptionListGroup> }
</DescriptionList>
</FlexItem>
<FlexItem className="hwinfo-system-info-list-item" flex={{ default: 'flex_1' }}>
<DescriptionList className="pf-m-horizontal-on-md">
{ info.bios_vendor && <>
<DescriptionListGroup>
<DescriptionListTerm>{ _("BIOS") }</DescriptionListTerm>
<DescriptionListDescription>{ info.bios_vendor }</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{ _("BIOS version") }</DescriptionListTerm>
<DescriptionListDescription>{ info.bios_version }</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{ _("BIOS date") }</DescriptionListTerm>
<DescriptionListDescription>{ bios_date ? timeformat.date(bios_date) : info.bios_date }</DescriptionListDescription>
</DescriptionListGroup>
</> }
{ info.nproc !== undefined && <>
<DescriptionListGroup>
<DescriptionListTerm>{ _("CPU") }</DescriptionListTerm>
<DescriptionListDescription>{ (info.nproc > 1) ? `${info.nproc}x ${info.cpu_model}` : info.cpu_model }</DescriptionListDescription>
</DescriptionListGroup>
{ onSecurityClick !== undefined && <DescriptionListGroup>
<DescriptionListTerm>{ _("CPU security") }</DescriptionListTerm>
<DescriptionListDescription>{ mitigations }</DescriptionListDescription>
</DescriptionListGroup>}
</> }
</DescriptionList>
</FlexItem>
</Flex>
);
};
let cachedMitigations;
function availableMitigations() {
if (cachedMitigations !== undefined)
return Promise.resolve(cachedMitigations);
const promises = [cockpit.spawn(["lscpu"], { environ: ["LC_ALL=C.UTF-8"], }), cockpit.file("/proc/cmdline").read()];
return Promise.all(promises).then(values => {
let threads_per_core;
try {
threads_per_core = Number(values[0].split('\n')
.find(l => l.indexOf('Thread(s) per core:') !== -1)
.split(':')[1]);
} catch (e) {
console.warn(e);
return { available: false };
}
const nosmt_enabled = (values[1].indexOf("nosmt") !== -1 && values[1].indexOf("nosmt=") === -1) || values[1].indexOf("nosmt=force") !== -1;
const nosmt_available = threads_per_core > 1 && (values[1].indexOf("nosmt=") === -1 || values[1].indexOf("nosmt=force") !== -1);
const mitigations_match = values[1].match(/\bmitigations=(\S*)\b/);
cachedMitigations = {
available: nosmt_available,
nosmt_enabled,
mitigations_arg: mitigations_match ? mitigations_match[1] : undefined,
};
return cachedMitigations;
});
}
const CPUSecurityMitigationsDialog = () => {
const [nosmt, setNoSMT] = React.useState(undefined);
const [alert, setAlert] = React.useState(undefined);
const [rebooting, setRebooting] = React.useState(false);
useInit(() => {
availableMitigations().then(({ available, nosmt_enabled }) => {
setNoSMT(nosmt_enabled);
});
});
const saveAndReboot = () => {
let options = [];
if (nosmt) {
options = ['set', 'nosmt'];
} else {
const ma = cachedMitigations.mitigations_arg;
if (ma && ma.indexOf("nosmt") >= 0) {
const new_args = ma.split(',').filter(opt => opt !== 'nosmt');
options = ['set', 'mitigations=' + new_args.join(',')];
} else {
options = ['remove', 'nosmt'];
}
}
cockpit.script(kernelopt_sh, options, { superuser: "require", err: "message" })
.then(() => {
cockpit.spawn(["shutdown", "--reboot", "now"], { superuser: "require", err: "message" })
.catch(error => { setRebooting(false); setAlert(error.message) });
})
.catch(error => { setRebooting(false); setAlert(error.message) });
setRebooting(true);
};
const Dialogs = useDialogs();
const rows = [];
if (nosmt !== undefined) {
rows.push(
<DataListItem key="nosmt">
<DataListItemRow>
<DataListItemCells
dataListCells={[
<DataListCell key="primary content">
<span>
<div className='nosmt-heading'>{ _("Disable simultaneous multithreading") } (nosmt)</div>
<small className='nosmt-read-more-link'>
<a href="https://access.redhat.com/security/vulnerabilities/L1TF" target="_blank" rel="noopener noreferrer">
<ExternalLinkAltIcon /> { _("Read more...") }
</a>
</small>
</span>
</DataListCell>,
]}
/>
<DataListAction>
<div id="nosmt-switch">
<Switch isDisabled={rebooting}
onChange={(_event, value) => setNoSMT(value)}
isChecked={nosmt} />
</div>
</DataListAction>
</DataListItemRow>
</DataListItem>
);
}
const footer = (
<ModalFooter>
<Button variant='danger' isDisabled={rebooting || nosmt === undefined} onClick={saveAndReboot}>
{ _("Save and reboot") }
</Button>
<Button variant='link' className='btn-cancel' isDisabled={rebooting} onClick={Dialogs.close}>
{ _("Cancel") }
</Button>
</ModalFooter>
);
return (
<Modal isOpen id="cpu-mitigations-dialog"
position="top" variant="medium"
onClose={Dialogs.close}>
<ModalHeader title={ _("CPU security toggles") } />
<ModalBody>
<Content className='cpu-mitigations-dialog-info' component={ContentVariants.p}>
{ _("Software-based workarounds help prevent CPU security issues. These mitigations have the side effect of reducing performance. Change these settings at your own risk.") }
</Content>
<DataList>
{ rows }
</DataList>
{ alert !== undefined &&
<Alert variant="danger"
actionClose={<AlertActionCloseButton onClose={() => setAlert(undefined)} />}
title={alert} />}
</ModalBody>
{footer}
</Modal>
);
};
const HardwareInfo = ({ info }) => {
const [mitigationsAvailable, setMitigationsAvailable] = React.useState(false);
useInit(() => {
availableMitigations().then(({ available }) => setMitigationsAvailable(available));
});
const Dialogs = useDialogs();
let pci = null;
let memory = null;
if (info.pci.length > 0) {
const sortedPci = info.pci.concat();
pci = (
<ListingTable aria-label={ _("PCI") }
sortBy={{ index: 0, direction: SortByDirection.asc }}
columns={ [
{ title: _("Class"), sortable: true },
{ title: _("Model"), sortable: true },
{ title: _("Vendor"), sortable: true },
{ title: _("Slot"), sortable: true }
] }
rows={ sortedPci.map(dev => ({
props: { key: dev.slot },
columns: [dev.cls, dev.model, dev.vendor, dev.slot]
}))} />
);
}
if (info.memory.length > 0) {
memory = (
<ListingTable aria-label={ _("Memory") }
columns={ [_("ID"), _("Memory technology"), _("Type"), _("Size"), _("State"), _("Rank"), _("Speed")]}
rows={ info.memory.map(dimm => ({
props: { key: dimm.locator },
columns: [dimm.locator, dimm.technology, dimm.type, dimm.size, dimm.state, dimm.rank, dimm.speed]
})) } />
);
} else if (!superuser.allowed) {
memory = (<EmptyState>
{_("Viewing memory information requires administrative access.")}
</EmptyState>);
}
return (
<Page className='pf-m-no-sidebar'>
<PageBreadcrumb hasBodyWrapper={false} stickyOnBreakpoint={{ default: "top" }}>
<Breadcrumb>
<BreadcrumbItem onClick={ () => cockpit.jump("/system", cockpit.transport.host)} className="pf-v6-c-breadcrumb__link">{ _("Overview") }</BreadcrumbItem>
<BreadcrumbItem isActive>{ _("Hardware information") }</BreadcrumbItem>
</Breadcrumb>
</PageBreadcrumb>
<PageSection hasBodyWrapper={false}>
<Gallery hasGutter>
<Card isPlain>
<CardHeader>
<CardTitle component="h2">{_("System information")}</CardTitle>
</CardHeader>
<CardBody>
<SystemInfo info={info.system}
onSecurityClick={mitigationsAvailable
? () => Dialogs.show(<CPUSecurityMitigationsDialog />)
: undefined } />
</CardBody>
</Card>
<Card isPlain id="pci-listing">
<CardHeader>
<CardTitle component="h2">{_("PCI")}</CardTitle>
</CardHeader>
<CardBody className="contains-list">
{ pci }
</CardBody>
</Card>
<Card isPlain id="memory-listing">
<CardHeader>
<CardTitle component="h2">{_("Memory")}</CardTitle>
</CardHeader>
<CardBody className="contains-list">
{ memory }
</CardBody>
</Card>
</Gallery>
</PageSection>
</Page>
);
};
document.addEventListener("DOMContentLoaded", () => {
document.title = cockpit.gettext(document.title);
detect().then(info => {
const root = createRoot(document.getElementById('hwinfo'));
root.render(<WithDialogs><HardwareInfo info={info} /></WithDialogs>);
});
});