LCOV - code coverage report
Current view: top level - pkg/systemd - hwinfo.jsx Coverage Total Hit
Test: cockpit Lines: 94.4 % 268 253
Test Date: 2026-07-17 07:33:01

            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 : });
        

Generated by: LCOV version 2.0-1