Line data Source code
1 : /*
2 : * Copyright (C) 2018 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 : import 'cockpit-dark-theme'; // once per page
7 : import '../lib/patternfly/patternfly-6-cockpit.scss';
8 : import 'polyfills'; // once per application
9 :
10 2 : import cockpit from "cockpit";
11 2 : import React from "react";
12 2 : import { createRoot } from 'react-dom/client';
13 :
14 : import * as timeformat from 'timeformat';
15 :
16 : import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
17 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
18 : import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
19 : import { DataList, DataListAction, DataListCell, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList/index.js";
20 : import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
21 : import { EmptyState } from "@patternfly/react-core/dist/esm/components/EmptyState/index.js";
22 : import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
23 : import { Gallery } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
24 : import { Page, PageBreadcrumb, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
25 : import { Content, ContentVariants } from "@patternfly/react-core/dist/esm/components/Content/index.js";
26 : import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js";
27 : import {
28 : Modal, ModalBody, ModalFooter, ModalHeader
29 : } from '@patternfly/react-core/dist/esm/components/Modal/index.js';
30 : import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
31 : import { ExternalLinkAltIcon } from "@patternfly/react-icons";
32 : import { SortByDirection } from "@patternfly/react-table";
33 : import { ListingTable } from "cockpit-components-table.jsx";
34 : import { WithDialogs, useDialogs } from "dialogs.jsx";
35 :
36 : import kernelopt_sh from "kernelopt.sh";
37 : import detect from "./hw-detect.js";
38 :
39 : import { superuser } from "superuser";
40 : import { PrivilegedButton } from "cockpit-components-privileged";
41 : import { useInit } from "hooks";
42 :
43 : import "./hwinfo.scss";
44 :
45 2 : const _ = cockpit.gettext;
46 :
47 2 : const SystemInfo = ({ info, onSecurityClick }) => {
48 1 : if ((!info.name || !info.version) && info.alt_name && info.alt_version) {
49 1 : info.name = info.alt_name;
50 1 : info.version = info.alt_version;
51 1 : }
52 :
53 2 : const mitigations = (
54 2 : <PrivilegedButton variant="link" buttonId="cpu_mitigations" tooltipId="tip-cpu-security"
55 2 : excuse={ _("The user $0 is not permitted to change cpu security mitigations") }
56 2 : onClick={ onSecurityClick }>
57 2 : { _("Mitigations") }
58 2 : </PrivilegedButton>
59 : );
60 :
61 2 : const bios_date = Date.parse(info.bios_date); // NaN for undefined, null, or invalid dates
62 :
63 2 : return (
64 2 : <Flex id="hwinfo-system-info-list" direction={{ default: 'column', sm: 'row' }}>
65 2 : <FlexItem className="hwinfo-system-info-list-item" flex={{ default: 'flex_1' }}>
66 2 : <DescriptionList className="pf-m-horizontal-on-md">
67 2 : { info.type &&
68 2 : <DescriptionListGroup>
69 2 : <DescriptionListTerm>{ _("Type") }</DescriptionListTerm>
70 2 : <DescriptionListDescription>{ info.type }</DescriptionListDescription>
71 2 : </DescriptionListGroup> }
72 2 : { info.name &&
73 2 : <DescriptionListGroup>
74 2 : <DescriptionListTerm>{ _("Name") }</DescriptionListTerm>
75 2 : <DescriptionListDescription>{ info.name }</DescriptionListDescription>
76 2 : </DescriptionListGroup> }
77 2 : { info.version &&
78 2 : <DescriptionListGroup>
79 2 : <DescriptionListTerm>{ _("Version") }</DescriptionListTerm>
80 2 : <DescriptionListDescription>{ info.version }</DescriptionListDescription>
81 2 : </DescriptionListGroup> }
82 2 : </DescriptionList>
83 2 : </FlexItem>
84 2 : <FlexItem className="hwinfo-system-info-list-item" flex={{ default: 'flex_1' }}>
85 2 : <DescriptionList className="pf-m-horizontal-on-md">
86 2 : { info.bios_vendor && <>
87 2 : <DescriptionListGroup>
88 2 : <DescriptionListTerm>{ _("BIOS") }</DescriptionListTerm>
89 2 : <DescriptionListDescription>{ info.bios_vendor }</DescriptionListDescription>
90 2 : </DescriptionListGroup>
91 2 : <DescriptionListGroup>
92 2 : <DescriptionListTerm>{ _("BIOS version") }</DescriptionListTerm>
93 2 : <DescriptionListDescription>{ info.bios_version }</DescriptionListDescription>
94 2 : </DescriptionListGroup>
95 2 : <DescriptionListGroup>
96 2 : <DescriptionListTerm>{ _("BIOS date") }</DescriptionListTerm>
97 1 : <DescriptionListDescription>{ bios_date ? timeformat.date(bios_date) : info.bios_date }</DescriptionListDescription>
98 2 : </DescriptionListGroup>
99 2 : </> }
100 2 : { info.nproc !== undefined && <>
101 2 : <DescriptionListGroup>
102 2 : <DescriptionListTerm>{ _("CPU") }</DescriptionListTerm>
103 1 : <DescriptionListDescription>{ (info.nproc > 1) ? `${info.nproc}x ${info.cpu_model}` : info.cpu_model }</DescriptionListDescription>
104 2 : </DescriptionListGroup>
105 2 : { onSecurityClick !== undefined && <DescriptionListGroup>
106 2 : <DescriptionListTerm>{ _("CPU security") }</DescriptionListTerm>
107 2 : <DescriptionListDescription>{ mitigations }</DescriptionListDescription>
108 2 : </DescriptionListGroup>}
109 2 : </> }
110 2 : </DescriptionList>
111 2 : </FlexItem>
112 2 : </Flex>
113 : );
114 2 : };
115 :
116 2 : let cachedMitigations;
117 :
118 2 : function availableMitigations() {
119 2 : if (cachedMitigations !== undefined)
120 2 : return Promise.resolve(cachedMitigations);
121 : /* nosmt */
122 2 : const promises = [cockpit.spawn(["lscpu"], { environ: ["LC_ALL=C.UTF-8"], }), cockpit.file("/proc/cmdline").read()];
123 2 : return Promise.all(promises).then(values => {
124 2 : let threads_per_core;
125 2 : try {
126 2 : threads_per_core = Number(values[0].split('\n')
127 2 : .find(l => l.indexOf('Thread(s) per core:') !== -1)
128 2 : .split(':')[1]);
129 1 : } catch (e) {
130 1 : console.warn(e);
131 1 : return { available: false };
132 1 : }
133 : /* "nosmt" and "nosmt=force" are valid */
134 1 : const nosmt_enabled = (values[1].indexOf("nosmt") !== -1 && values[1].indexOf("nosmt=") === -1) || values[1].indexOf("nosmt=force") !== -1;
135 : /* available if threads>1 and the cmdline is valid */
136 1 : const nosmt_available = threads_per_core > 1 && (values[1].indexOf("nosmt=") === -1 || values[1].indexOf("nosmt=force") !== -1);
137 2 : const mitigations_match = values[1].match(/\bmitigations=(\S*)\b/);
138 :
139 2 : cachedMitigations = {
140 2 : available: nosmt_available,
141 2 : nosmt_enabled,
142 1 : mitigations_arg: mitigations_match ? mitigations_match[1] : undefined,
143 2 : };
144 2 : return cachedMitigations;
145 2 : });
146 2 : }
147 :
148 1 : const CPUSecurityMitigationsDialog = () => {
149 1 : const [nosmt, setNoSMT] = React.useState(undefined);
150 1 : const [alert, setAlert] = React.useState(undefined);
151 1 : const [rebooting, setRebooting] = React.useState(false);
152 :
153 1 : useInit(() => {
154 1 : availableMitigations().then(({ available, nosmt_enabled }) => {
155 1 : setNoSMT(nosmt_enabled);
156 1 : });
157 1 : });
158 :
159 1 : const saveAndReboot = () => {
160 1 : let options = [];
161 1 : if (nosmt) {
162 1 : options = ['set', 'nosmt'];
163 0 : } else {
164 : // this may either be an argument of its own, or part of mitigations=
165 0 : const ma = cachedMitigations.mitigations_arg;
166 0 : if (ma && ma.indexOf("nosmt") >= 0) {
167 0 : const new_args = ma.split(',').filter(opt => opt != 'nosmt');
168 0 : options = ['set', 'mitigations=' + new_args.join(',')];
169 0 : } else {
170 0 : options = ['remove', 'nosmt'];
171 0 : }
172 0 : }
173 :
174 1 : cockpit.script(kernelopt_sh, options, { superuser: "require", err: "message" })
175 0 : .then(() => {
176 0 : cockpit.spawn(["shutdown", "--reboot", "now"], { superuser: "require", err: "message" })
177 0 : .catch(error => { setRebooting(false); setAlert(error.message) });
178 0 : })
179 1 : .catch(error => { setRebooting(false); setAlert(error.message) });
180 1 : setRebooting(true);
181 1 : };
182 :
183 1 : const Dialogs = useDialogs();
184 1 : const rows = [];
185 1 : if (nosmt !== undefined) {
186 1 : rows.push(
187 1 : <DataListItem key="nosmt">
188 1 : <DataListItemRow>
189 1 : <DataListItemCells
190 1 : dataListCells={[
191 1 : <DataListCell key="primary content">
192 1 : <span>
193 1 : <div className='nosmt-heading'>{ _("Disable simultaneous multithreading") } (nosmt)</div>
194 1 : <small className='nosmt-read-more-link'>
195 1 : <a href="https://access.redhat.com/security/vulnerabilities/L1TF" target="_blank" rel="noopener noreferrer">
196 1 : <ExternalLinkAltIcon /> { _("Read more...") }
197 1 : </a>
198 1 : </small>
199 1 : </span>
200 1 : </DataListCell>,
201 1 : ]}
202 1 : />
203 1 : <DataListAction>
204 1 : <div id="nosmt-switch">
205 1 : <Switch isDisabled={rebooting}
206 1 : onChange={(_event, value) => setNoSMT(value)}
207 1 : isChecked={nosmt} />
208 1 : </div>
209 1 : </DataListAction>
210 1 : </DataListItemRow>
211 1 : </DataListItem>
212 1 : );
213 1 : }
214 :
215 1 : const footer = (
216 1 : <ModalFooter>
217 1 : <Button variant='danger' isDisabled={rebooting || nosmt === undefined} onClick={saveAndReboot}>
218 1 : { _("Save and reboot") }
219 1 : </Button>
220 1 : <Button variant='link' className='btn-cancel' isDisabled={rebooting} onClick={Dialogs.close}>
221 1 : { _("Cancel") }
222 1 : </Button>
223 1 : </ModalFooter>
224 : );
225 :
226 1 : return (
227 1 : <Modal isOpen id="cpu-mitigations-dialog"
228 1 : position="top" variant="medium"
229 1 : onClose={Dialogs.close}>
230 1 : <ModalHeader title={ _("CPU security toggles") } />
231 1 : <ModalBody>
232 1 : <Content className='cpu-mitigations-dialog-info' component={ContentVariants.p}>
233 1 : { _("Software-based workarounds help prevent CPU security issues. These mitigations have the side effect of reducing performance. Change these settings at your own risk.") }
234 1 : </Content>
235 1 : <DataList>
236 1 : { rows }
237 1 : </DataList>
238 1 : { alert !== undefined &&
239 1 : <Alert variant="danger"
240 0 : actionClose={<AlertActionCloseButton onClose={() => setAlert(undefined)} />}
241 1 : title={alert} />}
242 1 : </ModalBody>
243 1 : {footer}
244 1 : </Modal>
245 : );
246 1 : };
247 :
248 2 : const HardwareInfo = ({ info }) => {
249 2 : const [mitigationsAvailable, setMitigationsAvailable] = React.useState(false);
250 :
251 2 : useInit(() => {
252 2 : availableMitigations().then(({ available }) => setMitigationsAvailable(available));
253 2 : });
254 :
255 2 : const Dialogs = useDialogs();
256 2 : let pci = null;
257 2 : let memory = null;
258 :
259 2 : if (info.pci.length > 0) {
260 2 : const sortedPci = info.pci.concat();
261 :
262 2 : pci = (
263 2 : <ListingTable aria-label={ _("PCI") }
264 2 : sortBy={{ index: 0, direction: SortByDirection.asc }}
265 2 : columns={ [
266 2 : { title: _("Class"), sortable: true },
267 2 : { title: _("Model"), sortable: true },
268 2 : { title: _("Vendor"), sortable: true },
269 2 : { title: _("Slot"), sortable: true }
270 2 : ] }
271 2 : rows={ sortedPci.map(dev => ({
272 2 : props: { key: dev.slot },
273 2 : columns: [dev.cls, dev.model, dev.vendor, dev.slot]
274 2 : }))} />
275 : );
276 2 : }
277 :
278 2 : if (info.memory.length > 0) {
279 2 : memory = (
280 2 : <ListingTable aria-label={ _("Memory") }
281 2 : columns={ [_("ID"), _("Memory technology"), _("Type"), _("Size"), _("State"), _("Rank"), _("Speed")]}
282 2 : rows={ info.memory.map(dimm => ({
283 2 : props: { key: dimm.locator },
284 2 : columns: [dimm.locator, dimm.technology, dimm.type, dimm.size, dimm.state, dimm.rank, dimm.speed]
285 2 : })) } />
286 : );
287 1 : } else if (!superuser.allowed) {
288 1 : memory = (<EmptyState>
289 1 : {_("Viewing memory information requires administrative access.")}
290 1 : </EmptyState>);
291 1 : }
292 :
293 2 : return (
294 2 : <Page className='pf-m-no-sidebar'>
295 2 : <PageBreadcrumb hasBodyWrapper={false} stickyOnBreakpoint={{ default: "top" }}>
296 2 : <Breadcrumb>
297 0 : <BreadcrumbItem onClick={ () => cockpit.jump("/system", cockpit.transport.host)} className="pf-v6-c-breadcrumb__link">{ _("Overview") }</BreadcrumbItem>
298 2 : <BreadcrumbItem isActive>{ _("Hardware information") }</BreadcrumbItem>
299 2 : </Breadcrumb>
300 2 : </PageBreadcrumb>
301 2 : <PageSection hasBodyWrapper={false}>
302 2 : <Gallery hasGutter>
303 2 : <Card isPlain>
304 2 : <CardHeader>
305 2 : <CardTitle component="h2">{_("System information")}</CardTitle>
306 2 : </CardHeader>
307 2 : <CardBody>
308 2 : <SystemInfo info={info.system}
309 2 : onSecurityClick={mitigationsAvailable
310 1 : ? () => Dialogs.show(<CPUSecurityMitigationsDialog />)
311 2 : : undefined } />
312 2 : </CardBody>
313 2 : </Card>
314 2 : <Card isPlain id="pci-listing">
315 2 : <CardHeader>
316 2 : <CardTitle component="h2">{_("PCI")}</CardTitle>
317 2 : </CardHeader>
318 2 : <CardBody className="contains-list">
319 2 : { pci }
320 2 : </CardBody>
321 2 : </Card>
322 2 : <Card isPlain id="memory-listing">
323 2 : <CardHeader>
324 2 : <CardTitle component="h2">{_("Memory")}</CardTitle>
325 2 : </CardHeader>
326 2 : <CardBody className="contains-list">
327 2 : { memory }
328 2 : </CardBody>
329 2 : </Card>
330 2 : </Gallery>
331 2 : </PageSection>
332 2 : </Page>
333 : );
334 2 : };
335 :
336 2 : document.addEventListener("DOMContentLoaded", () => {
337 2 : document.title = cockpit.gettext(document.title);
338 2 : detect().then(info => {
339 2 : const root = createRoot(document.getElementById('hwinfo'));
340 2 : root.render(<WithDialogs><HardwareInfo info={info} /></WithDialogs>);
341 2 : });
342 2 : });
|