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