Line data Source code
1 : /*
2 : * Copyright (C) 2016 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 : import '../lib/patternfly/patternfly-6-cockpit.scss';
7 : import cockpit from "cockpit";
8 :
9 3 : import React, { useEffect, useState } from "react";
10 : import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
11 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
12 : import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
13 : import { Card, CardBody, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
14 : import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
15 : import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
16 : import { Form, FormGroup, FormSection } from "@patternfly/react-core/dist/esm/components/Form/index.js";
17 : import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
18 : import { Page, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
19 : import { CodeBlockCode } from "@patternfly/react-core/dist/esm/components/CodeBlock/index.js";
20 : import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
21 : import {
22 : Modal, ModalBody, ModalFooter, ModalHeader
23 : } from '@patternfly/react-core/dist/esm/components/Modal/index.js';
24 : import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
25 : import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
26 : import { Content, ContentVariants } from "@patternfly/react-core/dist/esm/components/Content/index.js";
27 : import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
28 : import { Title } from "@patternfly/react-core/dist/esm/components/Title/index.js";
29 : import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
30 :
31 : import { useDialogs, DialogsContext } from "dialogs.jsx";
32 : import { read_os_release } from "os-release.js";
33 : import { fmt_to_fragments } from 'utils.jsx';
34 : import { FormHelper } from "cockpit-components-form-helper";
35 : import { ModalError } from 'cockpit-components-inline-notification.jsx';
36 : import { PrivilegedButton } from "cockpit-components-privileged";
37 : import { ModificationsExportDialog } from "cockpit-components-modifications";
38 :
39 3 : const _ = cockpit.gettext;
40 3 : const DEFAULT_KDUMP_PATH = "/var/crash";
41 :
42 0 : const exportAnsibleTask = (settings, os_release) => {
43 0 : const target = Object.keys(settings.targets)[0];
44 0 : const targetSettings = settings.targets[target];
45 0 : const kdump_core_collector = settings.core_collector;
46 :
47 0 : let role_name = "linux-system-roles";
48 0 : if (os_release?.PLATFORM_ID?.startsWith('platform:el') || os_release.ID_LIKE?.includes('rhel')) {
49 0 : role_name = "rhel-system-roles";
50 0 : }
51 :
52 0 : let ansible = `
53 : ---
54 : # Also available via https://galaxy.ansible.com/ui/standalone/roles/linux-system-roles/kdump/
55 0 : - name: install ${role_name}
56 : package:
57 0 : name: ${role_name}
58 : state: present
59 : delegate_to: 127.0.0.1
60 : become: true
61 : - name: run kdump system role
62 : include_role:
63 0 : name: ${role_name}.kdump
64 : vars:
65 0 : kdump_path: ${targetSettings.path || DEFAULT_KDUMP_PATH}
66 0 : kdump_core_collector: ${kdump_core_collector}`;
67 :
68 0 : if (target === "ssh") {
69 : // HACK: we should not have to specify kdump_ssh_user and kdump_ssh_user as it is in kdump_target.location
70 : // https://github.com/linux-system-roles/kdump/issues/184
71 0 : let ssh_user;
72 0 : let ssh_server;
73 0 : const parts = targetSettings.server.split('@');
74 0 : if (parts.length === 1) {
75 0 : ssh_user = "root";
76 0 : ssh_server = parts[0];
77 0 : } else if (parts.length === 2) {
78 0 : ssh_user = parts[0];
79 0 : ssh_server = parts[1];
80 0 : } else {
81 0 : throw new Error("ssh server contains two @ symbols");
82 0 : }
83 0 : ansible += `
84 : kdump_target:
85 : type: ssh
86 0 : kdump_sshkey: ${targetSettings.sshkey}
87 0 : kdump_ssh_server: ${ssh_server}
88 0 : kdump_ssh_user: ${ssh_user}`;
89 0 : } else if (target === "nfs") {
90 0 : ansible += `
91 : kdump_target:
92 : type: nfs
93 0 : location: ${targetSettings.server}:${targetSettings.export}
94 : `;
95 0 : } else if (target !== "local") {
96 : // target is unsupported
97 0 : throw new Error("Unsupported kdump target"); // not-covered: assertion
98 0 : }
99 :
100 0 : return ansible;
101 0 : };
102 :
103 3 : function getLocation(target, config) {
104 2 : let path = target.path || DEFAULT_KDUMP_PATH;
105 :
106 2 : if (target.type === "ssh") {
107 2 : path = `${target.server}:${path}`;
108 2 : } else if (target.type == "nfs") {
109 1 : if (!config.nfs_supports_directory) {
110 1 : path = '';
111 1 : }
112 1 : path = path[0] !== '/' ? '/' + path : path;
113 2 : path = `${target.server}:${target.export + path}`;
114 2 : }
115 :
116 3 : return path;
117 3 : }
118 :
119 2 : const KdumpSettingsModal = ({ settings, initialTarget, handleSave }) => {
120 2 : const Dialogs = useDialogs();
121 2 : const compressionAllowed = settings.compression?.allowed;
122 2 : const [isSaving, setIsSaving] = useState(false);
123 2 : const [error, setError] = useState(null);
124 2 : const [isFormValid, setFormValid] = useState(true);
125 2 : const [validationErrors, setValidationErrors] = useState({});
126 :
127 2 : const [storageLocation, setStorageLocation] = useState(Object.keys(settings.targets)[0]);
128 : // common options
129 2 : const [compressionEnabled, setCompressionEnabled] = useState(settings.compression?.enabled);
130 2 : const [directory, setDirectory] = useState(initialTarget.path || DEFAULT_KDUMP_PATH);
131 : // nfs and ssh
132 2 : const [server, setServer] = useState(settings.targets.nfs?.server || settings.targets.ssh?.server);
133 : // nfs
134 2 : const [exportPath, setExportPath] = useState(settings.targets.nfs?.export || "");
135 : // ssh
136 2 : const [sshkey, setSSHKey] = useState(settings.targets.ssh?.sshkey || "");
137 :
138 2 : useEffect(() => {
139 : // We can't use a ref in a functional component
140 2 : const elem = document.querySelector("#kdump-settings-form");
141 2 : if (elem)
142 2 : setFormValid(elem.checkValidity());
143 2 : }, [storageLocation, directory, sshkey, server, exportPath]);
144 :
145 2 : const changeStorageLocation = target => {
146 2 : setError(null);
147 2 : setDirectory(DEFAULT_KDUMP_PATH);
148 2 : setServer("");
149 2 : setStorageLocation(target);
150 2 : };
151 :
152 2 : const changeSSHKey = value => {
153 2 : if (value.trim() && !value.match("/.+")) {
154 2 : setValidationErrors({ sshkey: _("SSH key isn't a path") });
155 2 : } else {
156 2 : setValidationErrors({});
157 2 : }
158 2 : setSSHKey(value);
159 2 : };
160 :
161 2 : const saveSettings = () => {
162 2 : setError(null);
163 2 : setIsSaving(true);
164 2 : const newSettings = {
165 2 : compression: {
166 2 : allowed: compressionAllowed,
167 2 : enabled: compressionEnabled,
168 2 : },
169 2 : targets: {
170 2 : [storageLocation]: {
171 2 : type: storageLocation,
172 : // HACK: to not needlessly write a path /var/crash as this is the default,
173 : // set an empty string.
174 2 : path: directory === DEFAULT_KDUMP_PATH ? "" : directory,
175 2 : }
176 2 : },
177 2 : _internal: {
178 2 : ...settings._internal
179 2 : }
180 2 : };
181 :
182 2 : if (storageLocation === "ssh") {
183 2 : newSettings.targets.ssh.server = server;
184 2 : newSettings.targets.ssh.sshkey = sshkey;
185 2 : }
186 :
187 2 : if (storageLocation === "nfs") {
188 2 : newSettings.targets.nfs.server = server;
189 2 : newSettings.targets.nfs.export = exportPath;
190 2 : }
191 :
192 2 : handleSave(newSettings)
193 2 : .then(Dialogs.close)
194 2 : .finally(() => setIsSaving(false))
195 0 : .catch(error => {
196 0 : if (error.details) {
197 : // avoid bad summary like "systemd job RestartUnit ["kdump.service","replace"] failed with result failed"
198 : // if we have a more concrete journal and trim journal's `kdump: ` prefix.
199 0 : error.message = _("Unable to save settings");
200 0 : error.details = <CodeBlockCode>{ error.details.replaceAll(/\nkdump: /g, "\n") }</CodeBlockCode>;
201 0 : setError(error);
202 0 : } else {
203 : // without a journal, show the error as-is
204 0 : setError(new Error(cockpit.format(_("Unable to save settings: $0"), String(error))));
205 0 : }
206 0 : });
207 2 : };
208 :
209 2 : return (
210 2 : <Modal position="top" variant="small" id="kdump-settings-dialog" isOpen
211 2 : onClose={Dialogs.close}>
212 2 : <ModalHeader title={_("Crash dump location")} />
213 2 : <ModalBody>
214 0 : {error && <ModalError isExpandable
215 0 : dialogError={error.message || error}
216 0 : dialogErrorDetail={error.details} />}
217 2 : <Form id="kdump-settings-form" isHorizontal>
218 2 : <FormGroup fieldId="kdump-settings-location" label={_("Location")}>
219 2 : <FormSelect key="location" onChange={(_, val) => changeStorageLocation(val)}
220 2 : id="kdump-settings-location" value={storageLocation}>
221 2 : <FormSelectOption value='local'
222 2 : label={_("Local filesystem")} />
223 2 : <FormSelectOption value='ssh'
224 2 : label={_("Remote over SSH")} />
225 2 : <FormSelectOption value='nfs'
226 2 : label={_("Remote over NFS")} />
227 2 : </FormSelect>
228 2 : </FormGroup>
229 :
230 2 : {storageLocation === "local" &&
231 2 : <FormGroup fieldId="kdump-settings-local-directory" label={_("Directory")} isRequired>
232 2 : <TextInput id="kdump-settings-local-directory" key="directory"
233 2 : placeholder={DEFAULT_KDUMP_PATH} value={directory}
234 2 : data-stored={directory}
235 2 : onChange={(_event, value) => setDirectory(value)}
236 2 : isRequired />
237 2 : </FormGroup>
238 : }
239 :
240 2 : {storageLocation === "nfs" &&
241 2 : <>
242 2 : <FormGroup fieldId="kdump-settings-nfs-server" label={_("Server")} isRequired>
243 2 : <TextInput id="kdump-settings-nfs-server" key="server"
244 2 : placeholder="penguin.example.com" value={server}
245 2 : onChange={(_event, value) => setServer(value)} isRequired />
246 2 : </FormGroup>
247 2 : <FormGroup fieldId="kdump-settings-nfs-export" label={_("Export")} isRequired>
248 2 : <TextInput id="kdump-settings-nfs-export" key="export"
249 2 : placeholder="/export/cores" value={exportPath}
250 2 : onChange={(_event, value) => setExportPath(value)} isRequired />
251 2 : </FormGroup>
252 2 : {settings.nfs_supports_directory &&
253 1 : <FormGroup fieldId="kdump-settings-nfs-directory" label={_("Directory")} isRequired>
254 1 : <TextInput id="kdump-settings-nfs-directory" key="directory"
255 1 : placeholder={DEFAULT_KDUMP_PATH} value={directory}
256 1 : data-stored={directory}
257 1 : onChange={(_event, value) => setDirectory(value)}
258 1 : isRequired />
259 1 : </FormGroup>
260 : }
261 2 : </>
262 : }
263 :
264 2 : {storageLocation === "ssh" &&
265 2 : <>
266 2 : <FormGroup fieldId="kdump-settings-ssh-server" label={_("Server")} isRequired>
267 2 : <TextInput id="kdump-settings-ssh-server" key="server"
268 2 : placeholder="user@server.com" value={server}
269 2 : onChange={(_event, value) => setServer(value)} isRequired />
270 2 : </FormGroup>
271 :
272 2 : <FormGroup fieldId="kdump-settings-ssh-key" label={_("SSH key")}>
273 2 : <TextInput id="kdump-settings-ssh-key" key="ssh"
274 2 : placeholder="/root/.ssh/kdump_id_rsa" value={sshkey}
275 2 : onChange={(_event, value) => changeSSHKey(value)}
276 2 : validated={validationErrors.sshkey ? "error" : "default"} />
277 2 : <FormHelper helperTextInvalid={validationErrors.sshkey} />
278 2 : </FormGroup>
279 :
280 2 : <FormGroup fieldId="kdump-settings-ssh-directory" label={_("Directory")} isRequired>
281 2 : <TextInput id="kdump-settings-ssh-directory" key="directory"
282 2 : placeholder={DEFAULT_KDUMP_PATH} value={directory}
283 2 : data-stored={directory}
284 2 : onChange={(_event, value) => setDirectory(value)}
285 2 : isRequired />
286 2 : </FormGroup>
287 2 : </>
288 : }
289 :
290 2 : <FormSection>
291 2 : <FormGroup fieldId="kdump-settings-compression" label={_("Compression")} hasNoPaddingTop>
292 2 : <Checkbox id="kdump-settings-compression"
293 2 : isChecked={compressionEnabled}
294 2 : onChange={(_, c) => setCompressionEnabled(c)}
295 2 : isDisabled={!compressionAllowed}
296 2 : label={_("Compress crash dumps to save space")} />
297 2 : </FormGroup>
298 2 : </FormSection>
299 2 : </Form>
300 2 : </ModalBody>
301 2 : <ModalFooter>
302 2 : <Button variant="primary"
303 2 : isLoading={isSaving}
304 2 : isDisabled={isSaving || !isFormValid || Object.keys(validationErrors).length !== 0}
305 2 : onClick={saveSettings}>
306 2 : {_("Save changes")}
307 2 : </Button>
308 2 : <Button variant="link"
309 2 : isDisabled={isSaving}
310 2 : className="cancel"
311 2 : onClick={Dialogs.close}>
312 2 : {_("Cancel")}
313 2 : </Button>
314 2 : </ModalFooter>
315 2 : </Modal>);
316 2 : };
317 :
318 0 : const KdumpTestDialog = ({ verifyMessage, onCrashKernel }) => {
319 0 : const Dialogs = useDialogs();
320 0 : const [task, setTask] = useState(null);
321 0 : const [error, setError] = useState(null);
322 :
323 0 : function crash() {
324 0 : setError(null);
325 0 : setTask(onCrashKernel()
326 0 : .then(Dialogs.close)
327 0 : .catch(error => {
328 0 : setTask(null);
329 0 : setError(error);
330 0 : }));
331 0 : }
332 :
333 0 : return (
334 0 : <Modal position="top" variant="small" id="kdump-test-dialog" isOpen
335 0 : onClose={Dialogs.close}>
336 0 : <ModalHeader title={_("Test kdump settings")} titleIconVariant="warning" />
337 0 : <ModalBody>
338 0 : {error && <ModalError dialogError={error.message || error} />}
339 0 : <Content>
340 0 : <Content component={ContentVariants.p}>
341 0 : {_("Test kdump settings by crashing the kernel. This may take a while and the system might not automatically reboot. Do not purposefully crash the system while any important task is running.")}
342 0 : </Content>
343 0 : {verifyMessage && <Content component={ContentVariants.p}>
344 0 : {verifyMessage}
345 0 : </Content>}
346 0 : </Content>
347 0 : </ModalBody>
348 0 : <ModalFooter>
349 0 : <Button variant="danger"
350 0 : isLoading={!!task}
351 0 : isDisabled={!!task}
352 0 : onClick={crash}>
353 0 : {_("Crash system")}
354 0 : </Button>
355 0 : <Button variant="link"
356 0 : isDisabled={!!task}
357 0 : onClick={Dialogs.close}>
358 0 : {_("Cancel")}
359 0 : </Button>
360 0 : </ModalFooter>
361 0 : </Modal>);
362 0 : };
363 :
364 : /* Show kdump status of the system and offer options to change or test the state
365 : * Expected properties:
366 : * kdumpActive kdump service status
367 : * onSetServiceState called when the OnOff state is toggled (for kdumpActive), parameter: desired state
368 : * stateChanging whether we're currently waiting for our last change to take effect
369 : * onSaveSettings called with current dialog settings when the user clicks Save
370 : * kdumpStatus object as described in kdump-client
371 : * reservedMemory memory reserved at boot time for kdump use
372 : * onCrashKernel callback to crash the kernel via kdumpClient, expects a promise
373 : */
374 3 : export class KdumpPage extends React.Component {
375 3 : static contextType = DialogsContext;
376 :
377 3 : constructor(props) {
378 3 : super(props);
379 3 : this.state = { os_release: null };
380 :
381 3 : this.handleTestSettingsClick = this.handleTestSettingsClick.bind(this);
382 3 : this.handleSettingsClick = this.handleSettingsClick.bind(this);
383 3 : this.handleAutomationClick = this.handleAutomationClick.bind(this);
384 3 : read_os_release().then(os_release => this.setState({ os_release }));
385 3 : }
386 :
387 0 : handleTestSettingsClick() {
388 0 : const Dialogs = this.context;
389 : // if we have multiple targets defined, the config is invalid
390 0 : const target = this.props.kdumpStatus.target;
391 0 : let verifyMessage;
392 0 : if (!target.multipleTargets) {
393 0 : const path = getLocation(target, this.props.kdumpStatus.config);
394 0 : if (target.type === "local") {
395 0 : verifyMessage = fmt_to_fragments(
396 0 : ' ' + _("Results of the crash will be stored in $0 as $1, if kdump is properly configured."),
397 0 : <span className="pf-v6-u-font-family-monospace-vf">{path}</span>,
398 0 : <span className="pf-v6-u-font-family-monospace-vf">vmcore</span>);
399 0 : } else if (target.type === "ssh" || target.type == "nfs") {
400 0 : verifyMessage = fmt_to_fragments(
401 0 : ' ' + _("Results of the crash will be copied through $0 to $1 as $2, if kdump is properly configured."),
402 0 : <span className="pf-v6-u-font-family-monospace-vf">{target.type === "ssh" ? "SSH" : "NFS"}</span>,
403 0 : <span className="pf-v6-u-font-family-monospace-vf">{path}</span>,
404 0 : <span className="pf-v6-u-font-family-monospace-vf">vmcore</span>);
405 0 : }
406 0 : }
407 :
408 0 : Dialogs.show(<KdumpTestDialog verifyMessage={verifyMessage}
409 0 : onCrashKernel={this.props.onCrashKernel} />);
410 0 : }
411 :
412 0 : handleServiceDetailsClick() {
413 0 : cockpit.jump("/system/services#/kdump.service", cockpit.transport.host);
414 0 : }
415 :
416 2 : handleSettingsClick() {
417 2 : const Dialogs = this.context;
418 2 : Dialogs.show(<KdumpSettingsModal settings={this.props.kdumpStatus.config}
419 2 : initialTarget={this.props.kdumpStatus.target}
420 2 : handleSave={this.props.onSaveSettings} />);
421 2 : }
422 :
423 0 : handleAutomationClick() {
424 0 : const Dialogs = this.context;
425 0 : let enableCrashKernel = '';
426 0 : let kdumpconf = this.props.exportConfig(this.props.kdumpStatus.config);
427 0 : kdumpconf = kdumpconf.replaceAll('$', '\\$');
428 0 : if (this.state.os_release.NAME?.includes('Fedora')) {
429 0 : enableCrashKernel = `
430 : # A reboot will be required if crashkernel was not set before
431 : kdumpctl reset-crashkernel`;
432 0 : }
433 0 : let shell;
434 0 : if (this.state.os_release.NAME?.includes('MicroOS')) {
435 0 : enableCrashKernel = `
436 : # A reboot will be required if crashkernel was not set before
437 : transactional-update setup-kdump`;
438 0 : shell = `
439 : cat > /etc/kdump.conf << EOF
440 0 : ${kdumpconf}
441 : EOF
442 0 : ${enableCrashKernel}
443 : `;
444 0 : } else {
445 0 : shell = `
446 : cat > /etc/kdump.conf << EOF
447 0 : ${kdumpconf}
448 : EOF
449 : systemctl enable --now kdump.service
450 0 : ${enableCrashKernel}
451 : `;
452 0 : }
453 :
454 0 : Dialogs.show(
455 0 : <ModificationsExportDialog
456 0 : ansible={ this.state.os_release.NAME?.includes('MicroOS') ? null : exportAnsibleTask(this.props.kdumpStatus.config, this.state.os_release)}
457 0 : shell={shell}
458 0 : onClose={Dialogs.close}
459 0 : />);
460 0 : }
461 :
462 3 : render() {
463 3 : let kdumpLocation = (
464 3 : <div className="dialog-wait-ct">
465 3 : <Spinner size="md" />
466 3 : <span>{ _("Loading...") }</span>
467 3 : </div>
468 : );
469 3 : let targetCanChange = false;
470 3 : if (this.props.kdumpStatus && this.props.kdumpStatus.target) {
471 : // if we have multiple targets defined, the config is invalid
472 3 : const target = this.props.kdumpStatus.target;
473 0 : if (target.multipleTargets) {
474 0 : kdumpLocation = _("invalid: multiple targets defined");
475 0 : } else {
476 3 : const locationPath = getLocation(target, this.props.kdumpStatus.config);
477 3 : if (target.type == "local") {
478 3 : kdumpLocation = cockpit.format(_("Local, $0"), locationPath);
479 3 : targetCanChange = true;
480 2 : } else if (target.type == "ssh") {
481 2 : kdumpLocation = cockpit.format(_("Remote over SSH, $0"), locationPath);
482 2 : targetCanChange = true;
483 2 : } else if (target.type == "nfs") {
484 2 : kdumpLocation = cockpit.format(_("Remote over NFS, $0"), locationPath);
485 2 : targetCanChange = true;
486 0 : } else if (target.type == "raw") {
487 0 : kdumpLocation = _("Raw to a device");
488 0 : } else if (target.type == "mount") {
489 : /* mount targets outside of nfs are too complex for the
490 : * current target dialog */
491 0 : kdumpLocation = _("On a mounted device");
492 0 : } else if (target.type == "ftp") {
493 1 : kdumpLocation = _("Remote over FTP");
494 1 : } else if (target.type == "sftp") {
495 1 : kdumpLocation = _("Remote over SFTP");
496 1 : } else if (target.type == "cifs") {
497 1 : kdumpLocation = _("Remote over CIFS/SMB");
498 1 : } else {
499 1 : kdumpLocation = _("No configuration found");
500 1 : }
501 3 : }
502 3 : }
503 : // this.storeLocation(this.props.kdumpStatus.config);
504 3 : const settingsLink = targetCanChange && <Button variant="link" isInline id="kdump-change-target" onClick={this.handleSettingsClick}>{_("Edit")}</Button>;
505 3 : let reservedMemory;
506 3 : if (this.props.reservedMemory === undefined) {
507 : // still waiting for result
508 3 : reservedMemory = (
509 3 : <div className="dialog-wait-ct">
510 3 : <Spinner size="md" />
511 3 : <span>{ _("Reading...") }</span>
512 3 : </div>
513 : );
514 3 : } else if (this.props.reservedMemory === 0) {
515 : // nothing reserved
516 3 : reservedMemory = <span>{_("None")} </span>;
517 0 : } else if (Number.isInteger(this.props.reservedMemory)) {
518 : // TODO: hint at using debug_mem_level to identify actual memory required?
519 0 : reservedMemory = <span>{cockpit.format_bytes(this.props.reservedMemory, { base2: true })}</span>;
520 0 : } else {
521 : // error while reading
522 0 : reservedMemory = null;
523 0 : }
524 :
525 3 : const serviceRunning = this.props.kdumpStatus?.target &&
526 3 : this.props.kdumpStatus?.installed &&
527 3 : this.props.kdumpStatus?.state === "running";
528 :
529 3 : let testButton;
530 0 : if (serviceRunning) {
531 0 : testButton = (
532 0 : <PrivilegedButton variant="secondary" isDanger
533 0 : excuse={ _("The user $0 is not permitted to test crash the kernel") }
534 0 : onClick={this.handleTestSettingsClick}>
535 0 : { _("Test configuration") }
536 0 : </PrivilegedButton>
537 : );
538 0 : } else {
539 3 : const tooltip = _("Test is only available while the kdump service is running.");
540 3 : testButton = (
541 3 : <Tooltip id="tip-test" content={tooltip}>
542 3 : <Button variant="secondary" isDanger isAriaDisabled>
543 3 : {_("Test configuration")}
544 3 : </Button>
545 3 : </Tooltip>
546 : );
547 3 : }
548 :
549 3 : let automationButton = null;
550 3 : if (this.props.kdumpStatus && this.props.kdumpStatus.config !== null && this.state.os_release !== null && targetCanChange) {
551 3 : automationButton = (
552 3 : <FlexItem align={{ md: 'alignRight' }}>
553 3 : <Button id="kdump-automation-script" variant="secondary" onClick={this.handleAutomationClick}>
554 3 : {_("View automation script")}
555 3 : </Button>
556 3 : </FlexItem>
557 : );
558 3 : }
559 :
560 3 : let kdumpSwitch;
561 3 : let kdumpSwitchHelper;
562 3 : if (!this.props.kdumpCmdlineEnabled) {
563 3 : kdumpSwitchHelper = _("Currently not supported");
564 0 : } else {
565 0 : kdumpSwitch = (<Switch isChecked={!!serviceRunning}
566 0 : onChange={this.props.onSetServiceState}
567 0 : aria-label={_("kdump status")}
568 0 : label={serviceRunning ? _("Enabled") : _("Disabled")}
569 0 : isDisabled={this.props.stateChanging} />);
570 0 : }
571 :
572 3 : let alertMessage;
573 3 : let alertDetail;
574 3 : if (!this.props.stateChanging && this.props.kdumpStatus && this.props.kdumpStatus.installed !== undefined) {
575 3 : if (this.props.kdumpStatus.installed) {
576 3 : if (this.props.reservedMemory == 0) {
577 3 : alertMessage = fmt_to_fragments(
578 3 : _("Kernel did not boot with the $0 setting"),
579 3 : <span className="pf-v6-u-font-family-monospace-vf">crashkernel</span>
580 3 : );
581 3 : alertDetail = fmt_to_fragments(
582 3 : _("Reserve memory at boot time by setting a '$0' option on the kernel command line. For example, append '$1' to $2 in $3 or use your distribution's kernel argument editor."),
583 3 : <span className="pf-v6-u-font-family-monospace-vf">crashkernel</span>,
584 3 : <span className="pf-v6-u-font-family-monospace-vf">crashkernel=512M</span>,
585 3 : <span className="pf-v6-u-font-family-monospace-vf">GRUB_CMDLINE_LINUX</span>,
586 3 : <span className="pf-v6-u-font-family-monospace-vf">/etc/default/grub</span>
587 3 : );
588 0 : } else if (this.props.kdumpStatus.state == "failed") {
589 0 : alertMessage = (
590 0 : <>
591 0 : {_("Service has an error")}
592 0 : <Button variant="link" isInline className="pf-v6-u-ml-sm" onClick={this.handleServiceDetailsClick}>{_("more details")}</Button>
593 0 : </>
594 : );
595 0 : }
596 0 : } else {
597 0 : alertMessage = _("Kdump service is not installed.");
598 0 : alertDetail = fmt_to_fragments(
599 0 : _("Install the $0 package."),
600 0 : <span className="pf-v6-u-font-family-monospace-vf">kexec-tools</span>
601 0 : );
602 0 : }
603 3 : }
604 3 : return (
605 3 : <Page className="pf-m-no-sidebar">
606 3 : <PageSection hasBodyWrapper={false}>
607 3 : <Flex spaceItems={{ default: 'spaceItemsMd' }} alignItems={{ default: 'alignItemsCenter' }}>
608 3 : <Title headingLevel="h2" size="3xl">
609 3 : {_("Kernel crash dump")}
610 3 : </Title>
611 3 : {kdumpSwitch}
612 3 : {kdumpSwitchHelper &&
613 3 : <HelperText className="subtle-helper-text">
614 3 : <HelperTextItem>{kdumpSwitchHelper}</HelperTextItem>
615 3 : </HelperText>}
616 3 : {automationButton}
617 3 : </Flex>
618 3 : </PageSection>
619 3 : <PageSection hasBodyWrapper={false}>
620 :
621 3 : {alertMessage &&
622 3 : <Alert variant='danger'
623 3 : className="pf-v6-u-mb-md"
624 3 : isLiveRegion={this.props.isLiveRegion}
625 3 : isInline
626 3 : title={alertMessage}>
627 3 : {alertDetail}
628 3 : </Alert>
629 : }
630 3 : <Card isPlain>
631 3 : <CardTitle>
632 3 : <Title headingLevel="h4" size="xl">
633 3 : {_("Kdump settings")}
634 3 : </Title>
635 3 : </CardTitle>
636 3 : <CardBody>
637 3 : <DescriptionList className="pf-m-horizontal-on-sm">
638 3 : <DescriptionListGroup>
639 3 : <DescriptionListTerm>{_("Reserved memory")}</DescriptionListTerm>
640 3 : <DescriptionListDescription>
641 3 : {reservedMemory}
642 3 : </DescriptionListDescription>
643 3 : </DescriptionListGroup>
644 :
645 3 : <DescriptionListGroup>
646 3 : <DescriptionListTerm>{_("Crash dump location")}</DescriptionListTerm>
647 3 : <DescriptionListDescription>
648 3 : <Flex spaceItems={{ default: 'spaceItemsSm' }}>
649 3 : <span id="kdump-target-info">{ kdumpLocation }</span>
650 3 : {settingsLink}
651 3 : </Flex>
652 3 : </DescriptionListDescription>
653 3 : </DescriptionListGroup>
654 :
655 3 : <DescriptionListGroup>
656 3 : <DescriptionListTerm />
657 3 : <DescriptionListDescription>
658 3 : {testButton}
659 3 : </DescriptionListDescription>
660 3 : </DescriptionListGroup>
661 3 : </DescriptionList>
662 3 : </CardBody>
663 3 : </Card>
664 3 : </PageSection>
665 3 : </Page>
666 : );
667 3 : }
668 3 : }
|