LCOV - code coverage report
Current view: top level - pkg/systemd/overview-cards - configurationCard.jsx Coverage Total Hit
Test: cockpit Lines: 23.4 % 222 52
Test Date: 2026-06-17 06:28:00

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2019 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5           36 : import React, { useState } from 'react';
       6              : import { Card, CardBody, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
       7              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
       8              : import {
       9              :     Modal, ModalBody, ModalFooter, ModalHeader
      10              : } from '@patternfly/react-core/dist/esm/components/Modal/index.js';
      11              : import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
      12              : import { Form, FormGroup, FormHelperText } from "@patternfly/react-core/dist/esm/components/Form/index.js";
      13              : import { HelperText, HelperTextItem, } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
      14              : import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js";
      15              : import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
      16              : 
      17              : import host_keys_script from "./ssh-list-host-keys.sh";
      18           36 : import cockpit from "cockpit";
      19              : import { superuser } from "superuser";
      20              : import { useObject, useEvent } from "hooks.js";
      21              : import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
      22              : import { ServerTimeConfig } from 'serverTime.js';
      23              : import { RealmdClient, RealmButton } from "./realmd.jsx";
      24              : import { TunedPerformanceProfile } from './tuned-dialog.jsx';
      25              : import { CryptoPolicyRow } from './cryptoPolicies.jsx';
      26              : import { useDialogs } from "dialogs.jsx";
      27              : import { useInit } from "hooks";
      28              : 
      29              : import "./configurationCard.scss";
      30              : 
      31           36 : const _ = cockpit.gettext;
      32              : 
      33           36 : export const ConfigurationCard = ({ hostname }) => {
      34           36 :     const Dialogs = useDialogs();
      35           36 :     const realmd_client = useObject(() => new RealmdClient(), null, []);
      36           36 :     useEvent(realmd_client, "changed");
      37              : 
      38           30 :     const hostname_button = (superuser.allowed && realmd_client.allowHostnameChange())
      39              :         ? (
      40           30 :             <Button id="system_information_hostname_button" variant="link"
      41            0 :                     onClick={() => Dialogs.show(<PageSystemInformationChangeHostname />)}
      42           30 :                     isInline aria-label="edit hostname">
      43            6 :                 {hostname !== "" ? _("edit") : _("Set hostname")}
      44           30 :             </Button>)
      45           36 :         : null;
      46              : 
      47           36 :     return (
      48           36 :         <Card className="system-configuration">
      49           36 :             <CardTitle>{_("Configuration")}</CardTitle>
      50           36 :             <CardBody>
      51           36 :                 <table className="pf-v6-c-table pf-m-grid-md pf-m-compact">
      52           36 :                     <tbody className="pf-v6-c-table__tbody">
      53           36 :                         <tr className="pf-v6-c-table__tr">
      54           36 :                             <th className="pf-v6-c-table__th" scope="row">{_("Hostname")}</th>
      55           36 :                             <td className="pf-v6-c-table__td">
      56           32 :                                 {hostname && <span id="system_information_hostname_text">{hostname}</span>}
      57           36 :                                 <span>{hostname_button}</span>
      58           36 :                             </td>
      59           36 :                         </tr>
      60              : 
      61           36 :                         <tr className="pf-v6-c-table__tr">
      62           36 :                             <th className="pf-v6-c-table__th" scope="row">{_("System time")}</th>
      63           36 :                             <td className="pf-v6-c-table__td" data-label="system-time"><ServerTimeConfig /></td>
      64           36 :                         </tr>
      65              : 
      66           36 :                         <tr className="pf-v6-c-table__tr">
      67           36 :                             <th className="pf-v6-c-table__th" scope="row">{_("Domain")}</th>
      68           36 :                             <td className="pf-v6-c-table__td"><RealmButton realmd_client={realmd_client} /></td>
      69           36 :                         </tr>
      70              : 
      71           36 :                         <tr className="pf-v6-c-table__tr">
      72           36 :                             <th className="pf-v6-c-table__th" scope="row">{_("Performance profile")}</th>
      73           36 :                             <td className="pf-v6-c-table__td"><TunedPerformanceProfile /></td>
      74           36 :                         </tr>
      75              : 
      76           36 :                         <CryptoPolicyRow />
      77              : 
      78           36 :                         <tr className="pf-v6-c-table__tr">
      79           36 :                             <th className="pf-v6-c-table__th" scope="row">{_("Secure shell keys")}</th>
      80           36 :                             <td className="pf-v6-c-table__td">
      81           36 :                                 <Button variant="link" isInline id="system-ssh-keys-link"
      82            0 :                                             onClick={() => Dialogs.show(<SystemInformationSshKeys />)}>
      83           36 :                                     {_("Show fingerprints")}
      84           36 :                                 </Button>
      85           36 :                             </td>
      86           36 :                         </tr>
      87           36 :                     </tbody>
      88           36 :                 </table>
      89           36 :             </CardBody>
      90           36 :         </Card>
      91              :     );
      92           36 : };
      93              : 
      94            0 : const SystemInformationSshKeys = () => {
      95            0 :     const Dialogs = useDialogs();
      96            0 :     const [keys, setKeys] = useState([]);
      97            0 :     const [error, setError] = useState("");
      98            0 :     const [loading, setLoading] = useState(true);
      99              : 
     100            0 :     function keysUpdate() {
     101            0 :         cockpit.script(host_keys_script, [], { superuser: "try", err: "message" })
     102            0 :                 .then(data => {
     103            0 :                     const seen = {};
     104            0 :                     const keys = {};
     105              : 
     106            0 :                     data.trim().split("\n")
     107            0 :                             .forEach(line => {
     108            0 :                                 if (!line)
     109            0 :                                     return;
     110              : 
     111            0 :                                 const parts = line.trim().split(" ");
     112            0 :                                 const fp = parts[1];
     113            0 :                                 if (!seen[fp]) {
     114            0 :                                     seen[fp] = fp;
     115            0 :                                     let title = parts[parts.length - 1];
     116            0 :                                     if (title) {
     117            0 :                                         const m = title.match(/^\((.*)\)$/);
     118            0 :                                         if (m && m[1])
     119            0 :                                             title = m[1];
     120            0 :                                     }
     121            0 :                                     if (!keys[title])
     122            0 :                                         keys[title] = [];
     123            0 :                                     keys[title].push(fp);
     124            0 :                                 }
     125            0 :                             });
     126              : 
     127            0 :                     let arr = Object.keys(keys);
     128            0 :                     arr.sort();
     129            0 :                     arr = arr.map(function(k) {
     130            0 :                         return { title: k, fps: keys[k] };
     131            0 :                     });
     132              : 
     133            0 :                     setKeys(arr);
     134            0 :                     setLoading(false);
     135            0 :                     setError("");
     136            0 :                 })
     137            0 :                 .catch(function(ex) {
     138            0 :                     setLoading(false);
     139            0 :                     setError(cockpit.format(_("failed to list ssh host keys: $0"), ex.message));
     140            0 :                 });
     141            0 :     }
     142              : 
     143            0 :     function create_keyUpdater() {
     144              :         /*
     145              :          * Yes, we do refresh the keys while the dialog is open.
     146              :          * It may occur that sshd is not running at the point when
     147              :          * we try, or in rare cases the keys may change.
     148              :          */
     149            0 :         keysUpdate();
     150            0 :         return window.setInterval(keysUpdate, 10 * 1000);
     151            0 :     }
     152              : 
     153            0 :     function destroy_keyUpdater(interval) {
     154            0 :         window.clearInterval(interval);
     155            0 :     }
     156              : 
     157            0 :     useObject(create_keyUpdater, destroy_keyUpdater, []);
     158              : 
     159            0 :     let body = null;
     160            0 :     if (error)
     161            0 :         body = <Alert variant='danger' isInline title={_("Loading of SSH keys failed")}>
     162            0 :             <p>{_("Error message")}: {error}</p>
     163            0 :         </Alert>;
     164            0 :     else if (loading)
     165            0 :         body = <EmptyStatePanel loading title={ _("Loading keys...") } />;
     166            0 :     else if (!keys.length)
     167            0 :         body = <EmptyStatePanel title={ _("No host keys found.") } />;
     168              :     else
     169            0 :         body = <List isPlain isBordered>
     170            0 :             {keys.map(key =>
     171            0 :                 <ListItem key={key.title}>
     172            0 :                     <h4>{key.title}</h4>
     173            0 :                     {key.fps.map((fp, i) => <div key={i}><small>{fp}</small></div>)}
     174            0 :                 </ListItem>
     175            0 :             )}
     176            0 :         </List>;
     177              : 
     178            0 :     return (
     179            0 :         <Modal isOpen position="top" variant="medium"
     180            0 :                onClose={Dialogs.close}
     181            0 :                id="system_information_ssh_keys"
     182              :         >
     183            0 :             <ModalHeader title={_("Machine SSH key fingerprints")} />
     184            0 :             <ModalBody>
     185            0 :                 {body}
     186            0 :             </ModalBody>
     187            0 :             <ModalFooter>
     188            0 :                 <Button variant='secondary' onClick={Dialogs.close}>{_("Close")}</Button>
     189            0 :             </ModalFooter>
     190            0 :         </Modal>
     191              :     );
     192            0 : };
     193              : 
     194            0 : const PageSystemInformationChangeHostname = () => {
     195            0 :     const Dialogs = useDialogs();
     196            0 :     const [update_from_pretty, set_update_from_pretty] = useState(true);
     197            0 :     const [init_hostname, set_init_hostname] = useState("");
     198            0 :     const [hostname, set_hostname] = useState("");
     199            0 :     const [proxy, set_proxy] = useState(null);
     200            0 :     const [pretty, set_pretty] = useState("");
     201            0 :     const [init_pretty, set_init_pretty] = useState("");
     202            0 :     const [error, set_error] = useState([]);
     203              : 
     204            0 :     useInit(() => {
     205            0 :         const client = cockpit.dbus('org.freedesktop.hostname1', { superuser: "try" });
     206            0 :         const hostname_proxy = client.proxy();
     207              : 
     208            0 :         hostname_proxy.wait()
     209            0 :                 .then(() => {
     210            0 :                     const initial_hostname = hostname_proxy.StaticHostname || "";
     211            0 :                     const initial_pretty_hostname = hostname_proxy.PrettyHostname || "";
     212              : 
     213            0 :                     set_proxy(hostname_proxy);
     214            0 :                     set_hostname(initial_hostname);
     215            0 :                     set_init_hostname(initial_hostname);
     216            0 :                     set_pretty(initial_pretty_hostname);
     217            0 :                     set_init_pretty(initial_pretty_hostname);
     218            0 :                 });
     219            0 :     });
     220              : 
     221            0 :     function onPrettyChanged(value) {
     222              :         // Whenever the pretty host name has changed (e.g. the user has edited it), we compute a new
     223              :         // simple host name (e.g. 7bit ASCII, no special chars/spaces, lower case) from it
     224              : 
     225            0 :         set_pretty(value);
     226              : 
     227            0 :         if (update_from_pretty) {
     228            0 :             const old_hostname = hostname;
     229            0 :             const first_dot = old_hostname.indexOf(".");
     230            0 :             let new_hostname = value
     231            0 :                     .toLowerCase()
     232            0 :                     .replace(/['".]+/g, "")
     233            0 :                     .replace(/[^a-zA-Z0-9]+/g, "-");
     234            0 :             new_hostname = new_hostname.substring(0, 64);
     235            0 :             if (first_dot >= 0)
     236            0 :                 new_hostname = new_hostname + old_hostname.substring(first_dot);
     237            0 :             set_hostname(new_hostname);
     238            0 :         }
     239            0 :     }
     240              : 
     241            0 :     function onHostnameChanged(value) {
     242            0 :         const error = [];
     243            0 :         if (value.length > 64)
     244            0 :             error.push(_("Real host name must be 64 characters or less"));
     245            0 :         if (value.match(/[.a-z0-9-]*/)[0] !== value || value.indexOf("..") !== -1)
     246            0 :             error.push(_("Real host name can only contain lower-case characters, digits, dashes, and periods (with populated subdomains)"));
     247              : 
     248            0 :         set_hostname(value);
     249            0 :         set_update_from_pretty(false);
     250            0 :         set_error(error);
     251            0 :     }
     252              : 
     253            0 :     function onSubmit(event) {
     254            0 :         const one = proxy.call("SetStaticHostname", [hostname, true]);
     255            0 :         const two = proxy.call("SetPrettyHostname", [pretty, true]);
     256              : 
     257            0 :         Promise.all([one, two]).then(Dialogs.close);
     258              : 
     259            0 :         if (event)
     260            0 :             event.preventDefault();
     261            0 :         return false;
     262            0 :     }
     263              : 
     264            0 :     const disabled = error.length || (init_hostname == hostname && init_pretty == pretty);
     265            0 :     return (
     266            0 :         <Modal isOpen position="top" variant="medium"
     267            0 :                onClose={Dialogs.close}
     268            0 :                id="system_information_change_hostname"
     269              :         >
     270            0 :             <ModalHeader title={_("Change host name")} />
     271            0 :             <ModalBody>
     272            0 :                 <Form isHorizontal onSubmit={onSubmit}>
     273            0 :                     <FormGroup fieldId="sich-pretty-hostname" label={_("Pretty host name")}>
     274            0 :                         <TextInput id="sich-pretty-hostname" value={pretty} onChange={(_event, value) => onPrettyChanged(value)} />
     275            0 :                     </FormGroup>
     276            0 :                     <FormGroup fieldId="sich-hostname" label={_("Real host name")}>
     277            0 :                         <TextInput id="sich-hostname" value={hostname} onChange={(_event, value) => onHostnameChanged(value)} validated={error.length ? "error" : "default"} />
     278            0 :                         {error.length > 0 && <FormHelperText>
     279            0 :                             <HelperText>
     280            0 :                                 {error.map((err, i) =>
     281            0 :                                     <HelperTextItem key={i} variant="error">
     282            0 :                                         {err}
     283            0 :                                     </HelperTextItem>
     284            0 :                                 )}
     285            0 :                             </HelperText>
     286            0 :                         </FormHelperText>}
     287            0 :                     </FormGroup>
     288            0 :                 </Form>
     289            0 :             </ModalBody>
     290            0 :             <ModalFooter>
     291            0 :                 <Button variant='primary' isDisabled={disabled} onClick={onSubmit}>{_("Change")}</Button>
     292            0 :                 <Button variant='link' onClick={Dialogs.close}>{_("Cancel")}</Button>
     293            0 :             </ModalFooter>
     294            0 :         </Modal>
     295              :     );
     296            0 : };
        

Generated by: LCOV version 2.0-1