mirror of
https://github.com/cockpit-project/cockpit.git
synced 2026-11-03 21:49:50 -09:00
668 lines
31 KiB
JavaScript
668 lines
31 KiB
JavaScript
/*
|
|
* Copyright (C) 2016 Red Hat, Inc.
|
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
|
*/
|
|
|
|
import '../lib/patternfly/patternfly-6-cockpit.scss';
|
|
import cockpit from "cockpit";
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
|
|
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
|
|
import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
|
|
import { Card, CardBody, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
|
|
import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
|
|
import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
|
|
import { Form, FormGroup, FormSection } from "@patternfly/react-core/dist/esm/components/Form/index.js";
|
|
import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
|
|
import { Page, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
|
|
import { CodeBlockCode } from "@patternfly/react-core/dist/esm/components/CodeBlock/index.js";
|
|
import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
|
|
import {
|
|
Modal, ModalBody, ModalFooter, ModalHeader
|
|
} from '@patternfly/react-core/dist/esm/components/Modal/index.js';
|
|
import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
|
|
import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
|
|
import { Content, ContentVariants } from "@patternfly/react-core/dist/esm/components/Content/index.js";
|
|
import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
|
|
import { Title } from "@patternfly/react-core/dist/esm/components/Title/index.js";
|
|
import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
|
|
|
|
import { useDialogs, DialogsContext } from "dialogs.jsx";
|
|
import { read_os_release } from "os-release.js";
|
|
import { fmt_to_fragments } from 'utils.jsx';
|
|
import { FormHelper } from "cockpit-components-form-helper";
|
|
import { ModalError } from 'cockpit-components-inline-notification.jsx';
|
|
import { PrivilegedButton } from "cockpit-components-privileged";
|
|
import { ModificationsExportDialog } from "cockpit-components-modifications";
|
|
|
|
const _ = cockpit.gettext;
|
|
const DEFAULT_KDUMP_PATH = "/var/crash";
|
|
|
|
const exportAnsibleTask = (settings, os_release) => {
|
|
const target = Object.keys(settings.targets)[0];
|
|
const targetSettings = settings.targets[target];
|
|
const kdump_core_collector = settings.core_collector;
|
|
|
|
let role_name = "linux-system-roles";
|
|
if (os_release?.PLATFORM_ID?.startsWith('platform:el') || os_release.ID_LIKE?.includes('rhel')) {
|
|
role_name = "rhel-system-roles";
|
|
}
|
|
|
|
let ansible = `
|
|
---
|
|
# Also available via https://galaxy.ansible.com/ui/standalone/roles/linux-system-roles/kdump/
|
|
- name: install ${role_name}
|
|
package:
|
|
name: ${role_name}
|
|
state: present
|
|
delegate_to: 127.0.0.1
|
|
become: true
|
|
- name: run kdump system role
|
|
include_role:
|
|
name: ${role_name}.kdump
|
|
vars:
|
|
kdump_path: ${targetSettings.path || DEFAULT_KDUMP_PATH}
|
|
kdump_core_collector: ${kdump_core_collector}`;
|
|
|
|
if (target === "ssh") {
|
|
// HACK: we should not have to specify kdump_ssh_user and kdump_ssh_user as it is in kdump_target.location
|
|
// https://github.com/linux-system-roles/kdump/issues/184
|
|
let ssh_user;
|
|
let ssh_server;
|
|
const parts = targetSettings.server.split('@');
|
|
if (parts.length === 1) {
|
|
ssh_user = "root";
|
|
ssh_server = parts[0];
|
|
} else if (parts.length === 2) {
|
|
ssh_user = parts[0];
|
|
ssh_server = parts[1];
|
|
} else {
|
|
throw new Error("ssh server contains two @ symbols");
|
|
}
|
|
ansible += `
|
|
kdump_target:
|
|
type: ssh
|
|
kdump_sshkey: ${targetSettings.sshkey}
|
|
kdump_ssh_server: ${ssh_server}
|
|
kdump_ssh_user: ${ssh_user}`;
|
|
} else if (target === "nfs") {
|
|
ansible += `
|
|
kdump_target:
|
|
type: nfs
|
|
location: ${targetSettings.server}:${targetSettings.export}
|
|
`;
|
|
} else if (target !== "local") {
|
|
// target is unsupported
|
|
throw new Error("Unsupported kdump target"); // not-covered: assertion
|
|
}
|
|
|
|
return ansible;
|
|
};
|
|
|
|
function getLocation(target, config) {
|
|
let path = target.path || DEFAULT_KDUMP_PATH;
|
|
|
|
if (target.type === "ssh") {
|
|
path = `${target.server}:${path}`;
|
|
} else if (target.type === "nfs") {
|
|
if (!config.nfs_supports_directory) {
|
|
path = '';
|
|
}
|
|
path = path[0] !== '/' ? '/' + path : path;
|
|
path = `${target.server}:${target.export + path}`;
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
const KdumpSettingsModal = ({ settings, initialTarget, handleSave }) => {
|
|
const Dialogs = useDialogs();
|
|
const compressionAllowed = settings.compression?.allowed;
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [error, setError] = useState(null);
|
|
const [isFormValid, setFormValid] = useState(true);
|
|
const [validationErrors, setValidationErrors] = useState({});
|
|
|
|
const [storageLocation, setStorageLocation] = useState(Object.keys(settings.targets)[0]);
|
|
// common options
|
|
const [compressionEnabled, setCompressionEnabled] = useState(settings.compression?.enabled);
|
|
const [directory, setDirectory] = useState(initialTarget.path || DEFAULT_KDUMP_PATH);
|
|
// nfs and ssh
|
|
const [server, setServer] = useState(settings.targets.nfs?.server || settings.targets.ssh?.server);
|
|
// nfs
|
|
const [exportPath, setExportPath] = useState(settings.targets.nfs?.export || "");
|
|
// ssh
|
|
const [sshkey, setSSHKey] = useState(settings.targets.ssh?.sshkey || "");
|
|
|
|
useEffect(() => {
|
|
// We can't use a ref in a functional component
|
|
const elem = document.querySelector("#kdump-settings-form");
|
|
if (elem)
|
|
setFormValid(elem.checkValidity());
|
|
}, [storageLocation, directory, sshkey, server, exportPath]);
|
|
|
|
const changeStorageLocation = target => {
|
|
setError(null);
|
|
setDirectory(DEFAULT_KDUMP_PATH);
|
|
setServer("");
|
|
setStorageLocation(target);
|
|
};
|
|
|
|
const changeSSHKey = value => {
|
|
if (value.trim() && !value.match("/.+")) {
|
|
setValidationErrors({ sshkey: _("SSH key isn't a path") });
|
|
} else {
|
|
setValidationErrors({});
|
|
}
|
|
setSSHKey(value);
|
|
};
|
|
|
|
const saveSettings = () => {
|
|
setError(null);
|
|
setIsSaving(true);
|
|
const newSettings = {
|
|
compression: {
|
|
allowed: compressionAllowed,
|
|
enabled: compressionEnabled,
|
|
},
|
|
targets: {
|
|
[storageLocation]: {
|
|
type: storageLocation,
|
|
// HACK: to not needlessly write a path /var/crash as this is the default,
|
|
// set an empty string.
|
|
path: directory === DEFAULT_KDUMP_PATH ? "" : directory,
|
|
}
|
|
},
|
|
_internal: {
|
|
...settings._internal
|
|
}
|
|
};
|
|
|
|
if (storageLocation === "ssh") {
|
|
newSettings.targets.ssh.server = server;
|
|
newSettings.targets.ssh.sshkey = sshkey;
|
|
}
|
|
|
|
if (storageLocation === "nfs") {
|
|
newSettings.targets.nfs.server = server;
|
|
newSettings.targets.nfs.export = exportPath;
|
|
}
|
|
|
|
handleSave(newSettings)
|
|
.then(Dialogs.close)
|
|
.finally(() => setIsSaving(false))
|
|
.catch(error => {
|
|
if (error.details) {
|
|
// avoid bad summary like "systemd job RestartUnit ["kdump.service","replace"] failed with result failed"
|
|
// if we have a more concrete journal and trim journal's `kdump: ` prefix.
|
|
error.message = _("Unable to save settings");
|
|
error.details = <CodeBlockCode>{ error.details.replaceAll(/\nkdump: /g, "\n") }</CodeBlockCode>;
|
|
setError(error);
|
|
} else {
|
|
// without a journal, show the error as-is
|
|
setError(new Error(cockpit.format(_("Unable to save settings: $0"), String(error))));
|
|
}
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Modal position="top" variant="small" id="kdump-settings-dialog" isOpen
|
|
onClose={Dialogs.close}>
|
|
<ModalHeader title={_("Crash dump location")} />
|
|
<ModalBody>
|
|
{error && <ModalError isExpandable
|
|
dialogError={error.message || error}
|
|
dialogErrorDetail={error.details} />}
|
|
<Form id="kdump-settings-form" isHorizontal>
|
|
<FormGroup fieldId="kdump-settings-location" label={_("Location")}>
|
|
<FormSelect key="location" onChange={(_, val) => changeStorageLocation(val)}
|
|
id="kdump-settings-location" value={storageLocation}>
|
|
<FormSelectOption value='local'
|
|
label={_("Local filesystem")} />
|
|
<FormSelectOption value='ssh'
|
|
label={_("Remote over SSH")} />
|
|
<FormSelectOption value='nfs'
|
|
label={_("Remote over NFS")} />
|
|
</FormSelect>
|
|
</FormGroup>
|
|
|
|
{storageLocation === "local" &&
|
|
<FormGroup fieldId="kdump-settings-local-directory" label={_("Directory")} isRequired>
|
|
<TextInput id="kdump-settings-local-directory" key="directory"
|
|
placeholder={DEFAULT_KDUMP_PATH} value={directory}
|
|
data-stored={directory}
|
|
onChange={(_event, value) => setDirectory(value)}
|
|
isRequired />
|
|
</FormGroup>
|
|
}
|
|
|
|
{storageLocation === "nfs" &&
|
|
<>
|
|
<FormGroup fieldId="kdump-settings-nfs-server" label={_("Server")} isRequired>
|
|
<TextInput id="kdump-settings-nfs-server" key="server"
|
|
placeholder="penguin.example.com" value={server}
|
|
onChange={(_event, value) => setServer(value)} isRequired />
|
|
</FormGroup>
|
|
<FormGroup fieldId="kdump-settings-nfs-export" label={_("Export")} isRequired>
|
|
<TextInput id="kdump-settings-nfs-export" key="export"
|
|
placeholder="/export/cores" value={exportPath}
|
|
onChange={(_event, value) => setExportPath(value)} isRequired />
|
|
</FormGroup>
|
|
{settings.nfs_supports_directory &&
|
|
<FormGroup fieldId="kdump-settings-nfs-directory" label={_("Directory")} isRequired>
|
|
<TextInput id="kdump-settings-nfs-directory" key="directory"
|
|
placeholder={DEFAULT_KDUMP_PATH} value={directory}
|
|
data-stored={directory}
|
|
onChange={(_event, value) => setDirectory(value)}
|
|
isRequired />
|
|
</FormGroup>
|
|
}
|
|
</>
|
|
}
|
|
|
|
{storageLocation === "ssh" &&
|
|
<>
|
|
<FormGroup fieldId="kdump-settings-ssh-server" label={_("Server")} isRequired>
|
|
<TextInput id="kdump-settings-ssh-server" key="server"
|
|
placeholder="user@server.com" value={server}
|
|
onChange={(_event, value) => setServer(value)} isRequired />
|
|
</FormGroup>
|
|
|
|
<FormGroup fieldId="kdump-settings-ssh-key" label={_("SSH key")}>
|
|
<TextInput id="kdump-settings-ssh-key" key="ssh"
|
|
placeholder="/root/.ssh/kdump_id_rsa" value={sshkey}
|
|
onChange={(_event, value) => changeSSHKey(value)}
|
|
validated={validationErrors.sshkey ? "error" : "default"} />
|
|
<FormHelper helperTextInvalid={validationErrors.sshkey} />
|
|
</FormGroup>
|
|
|
|
<FormGroup fieldId="kdump-settings-ssh-directory" label={_("Directory")} isRequired>
|
|
<TextInput id="kdump-settings-ssh-directory" key="directory"
|
|
placeholder={DEFAULT_KDUMP_PATH} value={directory}
|
|
data-stored={directory}
|
|
onChange={(_event, value) => setDirectory(value)}
|
|
isRequired />
|
|
</FormGroup>
|
|
</>
|
|
}
|
|
|
|
<FormSection>
|
|
<FormGroup fieldId="kdump-settings-compression" label={_("Compression")} hasNoPaddingTop>
|
|
<Checkbox id="kdump-settings-compression"
|
|
isChecked={compressionEnabled}
|
|
onChange={(_, c) => setCompressionEnabled(c)}
|
|
isDisabled={!compressionAllowed}
|
|
label={_("Compress crash dumps to save space")} />
|
|
</FormGroup>
|
|
</FormSection>
|
|
</Form>
|
|
</ModalBody>
|
|
<ModalFooter>
|
|
<Button variant="primary"
|
|
isLoading={isSaving}
|
|
isDisabled={isSaving || !isFormValid || Object.keys(validationErrors).length !== 0}
|
|
onClick={saveSettings}>
|
|
{_("Save changes")}
|
|
</Button>
|
|
<Button variant="link"
|
|
isDisabled={isSaving}
|
|
className="cancel"
|
|
onClick={Dialogs.close}>
|
|
{_("Cancel")}
|
|
</Button>
|
|
</ModalFooter>
|
|
</Modal>);
|
|
};
|
|
|
|
const KdumpTestDialog = ({ verifyMessage, onCrashKernel }) => {
|
|
const Dialogs = useDialogs();
|
|
const [task, setTask] = useState(null);
|
|
const [error, setError] = useState(null);
|
|
|
|
function crash() {
|
|
setError(null);
|
|
setTask(onCrashKernel()
|
|
.then(Dialogs.close)
|
|
.catch(error => {
|
|
setTask(null);
|
|
setError(error);
|
|
}));
|
|
}
|
|
|
|
return (
|
|
<Modal position="top" variant="small" id="kdump-test-dialog" isOpen
|
|
onClose={Dialogs.close}>
|
|
<ModalHeader title={_("Test kdump settings")} titleIconVariant="warning" />
|
|
<ModalBody>
|
|
{error && <ModalError dialogError={error.message || error} />}
|
|
<Content>
|
|
<Content component={ContentVariants.p}>
|
|
{_("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.")}
|
|
</Content>
|
|
{verifyMessage && <Content component={ContentVariants.p}>
|
|
{verifyMessage}
|
|
</Content>}
|
|
</Content>
|
|
</ModalBody>
|
|
<ModalFooter>
|
|
<Button variant="danger"
|
|
isLoading={!!task}
|
|
isDisabled={!!task}
|
|
onClick={crash}>
|
|
{_("Crash system")}
|
|
</Button>
|
|
<Button variant="link"
|
|
isDisabled={!!task}
|
|
onClick={Dialogs.close}>
|
|
{_("Cancel")}
|
|
</Button>
|
|
</ModalFooter>
|
|
</Modal>);
|
|
};
|
|
|
|
/* Show kdump status of the system and offer options to change or test the state
|
|
* Expected properties:
|
|
* kdumpActive kdump service status
|
|
* onSetServiceState called when the OnOff state is toggled (for kdumpActive), parameter: desired state
|
|
* stateChanging whether we're currently waiting for our last change to take effect
|
|
* onSaveSettings called with current dialog settings when the user clicks Save
|
|
* kdumpStatus object as described in kdump-client
|
|
* reservedMemory memory reserved at boot time for kdump use
|
|
* onCrashKernel callback to crash the kernel via kdumpClient, expects a promise
|
|
*/
|
|
export class KdumpPage extends React.Component {
|
|
static contextType = DialogsContext;
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = { os_release: null };
|
|
|
|
this.handleTestSettingsClick = this.handleTestSettingsClick.bind(this);
|
|
this.handleSettingsClick = this.handleSettingsClick.bind(this);
|
|
this.handleAutomationClick = this.handleAutomationClick.bind(this);
|
|
read_os_release().then(os_release => this.setState({ os_release }));
|
|
}
|
|
|
|
handleTestSettingsClick() {
|
|
const Dialogs = this.context;
|
|
// if we have multiple targets defined, the config is invalid
|
|
const target = this.props.kdumpStatus.target;
|
|
let verifyMessage;
|
|
if (!target.multipleTargets) {
|
|
const path = getLocation(target, this.props.kdumpStatus.config);
|
|
if (target.type === "local") {
|
|
verifyMessage = fmt_to_fragments(
|
|
' ' + _("Results of the crash will be stored in $0 as $1, if kdump is properly configured."),
|
|
<span className="pf-v6-u-font-family-monospace-vf">{path}</span>,
|
|
<span className="pf-v6-u-font-family-monospace-vf">vmcore</span>);
|
|
} else if (target.type === "ssh" || target.type === "nfs") {
|
|
verifyMessage = fmt_to_fragments(
|
|
' ' + _("Results of the crash will be copied through $0 to $1 as $2, if kdump is properly configured."),
|
|
<span className="pf-v6-u-font-family-monospace-vf">{target.type === "ssh" ? "SSH" : "NFS"}</span>,
|
|
<span className="pf-v6-u-font-family-monospace-vf">{path}</span>,
|
|
<span className="pf-v6-u-font-family-monospace-vf">vmcore</span>);
|
|
}
|
|
}
|
|
|
|
Dialogs.show(<KdumpTestDialog verifyMessage={verifyMessage}
|
|
onCrashKernel={this.props.onCrashKernel} />);
|
|
}
|
|
|
|
handleServiceDetailsClick() {
|
|
cockpit.jump("/system/services#/kdump.service", cockpit.transport.host);
|
|
}
|
|
|
|
handleSettingsClick() {
|
|
const Dialogs = this.context;
|
|
Dialogs.show(<KdumpSettingsModal settings={this.props.kdumpStatus.config}
|
|
initialTarget={this.props.kdumpStatus.target}
|
|
handleSave={this.props.onSaveSettings} />);
|
|
}
|
|
|
|
handleAutomationClick() {
|
|
const Dialogs = this.context;
|
|
let enableCrashKernel = '';
|
|
let kdumpconf = this.props.exportConfig(this.props.kdumpStatus.config);
|
|
kdumpconf = kdumpconf.replaceAll('$', '\\$');
|
|
if (this.state.os_release.NAME?.includes('Fedora')) {
|
|
enableCrashKernel = `
|
|
# A reboot will be required if crashkernel was not set before
|
|
kdumpctl reset-crashkernel`;
|
|
}
|
|
let shell;
|
|
if (this.state.os_release.NAME?.includes('MicroOS')) {
|
|
enableCrashKernel = `
|
|
# A reboot will be required if crashkernel was not set before
|
|
transactional-update setup-kdump`;
|
|
shell = `
|
|
cat > /etc/kdump.conf << EOF
|
|
${kdumpconf}
|
|
EOF
|
|
${enableCrashKernel}
|
|
`;
|
|
} else {
|
|
shell = `
|
|
cat > /etc/kdump.conf << EOF
|
|
${kdumpconf}
|
|
EOF
|
|
systemctl enable --now kdump.service
|
|
${enableCrashKernel}
|
|
`;
|
|
}
|
|
|
|
Dialogs.show(
|
|
<ModificationsExportDialog
|
|
ansible={ this.state.os_release.NAME?.includes('MicroOS') ? null : exportAnsibleTask(this.props.kdumpStatus.config, this.state.os_release)}
|
|
shell={shell}
|
|
onClose={Dialogs.close}
|
|
/>);
|
|
}
|
|
|
|
render() {
|
|
let kdumpLocation = (
|
|
<div className="dialog-wait-ct">
|
|
<Spinner size="md" />
|
|
<span>{ _("Loading...") }</span>
|
|
</div>
|
|
);
|
|
let targetCanChange = false;
|
|
if (this.props.kdumpStatus && this.props.kdumpStatus.target) {
|
|
// if we have multiple targets defined, the config is invalid
|
|
const target = this.props.kdumpStatus.target;
|
|
if (target.multipleTargets) {
|
|
kdumpLocation = _("invalid: multiple targets defined");
|
|
} else {
|
|
const locationPath = getLocation(target, this.props.kdumpStatus.config);
|
|
if (target.type === "local") {
|
|
kdumpLocation = cockpit.format(_("Local, $0"), locationPath);
|
|
targetCanChange = true;
|
|
} else if (target.type === "ssh") {
|
|
kdumpLocation = cockpit.format(_("Remote over SSH, $0"), locationPath);
|
|
targetCanChange = true;
|
|
} else if (target.type === "nfs") {
|
|
kdumpLocation = cockpit.format(_("Remote over NFS, $0"), locationPath);
|
|
targetCanChange = true;
|
|
} else if (target.type === "raw") {
|
|
kdumpLocation = _("Raw to a device");
|
|
} else if (target.type === "mount") {
|
|
/* mount targets outside of nfs are too complex for the
|
|
* current target dialog */
|
|
kdumpLocation = _("On a mounted device");
|
|
} else if (target.type === "ftp") {
|
|
kdumpLocation = _("Remote over FTP");
|
|
} else if (target.type === "sftp") {
|
|
kdumpLocation = _("Remote over SFTP");
|
|
} else if (target.type === "cifs") {
|
|
kdumpLocation = _("Remote over CIFS/SMB");
|
|
} else {
|
|
kdumpLocation = _("No configuration found");
|
|
}
|
|
}
|
|
}
|
|
// this.storeLocation(this.props.kdumpStatus.config);
|
|
const settingsLink = targetCanChange && <Button variant="link" isInline id="kdump-change-target" onClick={this.handleSettingsClick}>{_("Edit")}</Button>;
|
|
let reservedMemory;
|
|
if (this.props.reservedMemory === undefined) {
|
|
// still waiting for result
|
|
reservedMemory = (
|
|
<div className="dialog-wait-ct">
|
|
<Spinner size="md" />
|
|
<span>{ _("Reading...") }</span>
|
|
</div>
|
|
);
|
|
} else if (this.props.reservedMemory === 0) {
|
|
// nothing reserved
|
|
reservedMemory = <span>{_("None")} </span>;
|
|
} else if (Number.isInteger(this.props.reservedMemory)) {
|
|
// TODO: hint at using debug_mem_level to identify actual memory required?
|
|
reservedMemory = <span>{cockpit.format_bytes(this.props.reservedMemory, { base2: true })}</span>;
|
|
} else {
|
|
// error while reading
|
|
reservedMemory = null;
|
|
}
|
|
|
|
const serviceRunning = this.props.kdumpStatus?.target &&
|
|
this.props.kdumpStatus?.installed &&
|
|
this.props.kdumpStatus?.state === "running";
|
|
|
|
let testButton;
|
|
if (serviceRunning) {
|
|
testButton = (
|
|
<PrivilegedButton variant="secondary" isDanger
|
|
excuse={ _("The user $0 is not permitted to test crash the kernel") }
|
|
onClick={this.handleTestSettingsClick}>
|
|
{ _("Test configuration") }
|
|
</PrivilegedButton>
|
|
);
|
|
} else {
|
|
const tooltip = _("Test is only available while the kdump service is running.");
|
|
testButton = (
|
|
<Tooltip id="tip-test" content={tooltip}>
|
|
<Button variant="secondary" isDanger isAriaDisabled>
|
|
{_("Test configuration")}
|
|
</Button>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
let automationButton = null;
|
|
if (this.props.kdumpStatus && this.props.kdumpStatus.config !== null && this.state.os_release !== null && targetCanChange) {
|
|
automationButton = (
|
|
<FlexItem align={{ md: 'alignRight' }}>
|
|
<Button id="kdump-automation-script" variant="secondary" onClick={this.handleAutomationClick}>
|
|
{_("View automation script")}
|
|
</Button>
|
|
</FlexItem>
|
|
);
|
|
}
|
|
|
|
let kdumpSwitch;
|
|
let kdumpSwitchHelper;
|
|
if (!this.props.kdumpCmdlineEnabled) {
|
|
kdumpSwitchHelper = _("Currently not supported");
|
|
} else {
|
|
kdumpSwitch = (<Switch isChecked={!!serviceRunning}
|
|
onChange={this.props.onSetServiceState}
|
|
aria-label={_("kdump status")}
|
|
label={serviceRunning ? _("Enabled") : _("Disabled")}
|
|
isDisabled={this.props.stateChanging} />);
|
|
}
|
|
|
|
let alertMessage;
|
|
let alertDetail;
|
|
if (!this.props.stateChanging && this.props.kdumpStatus && this.props.kdumpStatus.installed !== undefined) {
|
|
if (this.props.kdumpStatus.installed) {
|
|
if (this.props.reservedMemory === 0) {
|
|
alertMessage = fmt_to_fragments(
|
|
_("Kernel did not boot with the $0 setting"),
|
|
<span className="pf-v6-u-font-family-monospace-vf">crashkernel</span>
|
|
);
|
|
alertDetail = fmt_to_fragments(
|
|
_("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."),
|
|
<span className="pf-v6-u-font-family-monospace-vf">crashkernel</span>,
|
|
<span className="pf-v6-u-font-family-monospace-vf">crashkernel=512M</span>,
|
|
<span className="pf-v6-u-font-family-monospace-vf">GRUB_CMDLINE_LINUX</span>,
|
|
<span className="pf-v6-u-font-family-monospace-vf">/etc/default/grub</span>
|
|
);
|
|
} else if (this.props.kdumpStatus.state === "failed") {
|
|
alertMessage = (
|
|
<>
|
|
{_("Service has an error")}
|
|
<Button variant="link" isInline className="pf-v6-u-ml-sm" onClick={this.handleServiceDetailsClick}>{_("more details")}</Button>
|
|
</>
|
|
);
|
|
}
|
|
} else {
|
|
alertMessage = _("Kdump service is not installed.");
|
|
alertDetail = fmt_to_fragments(
|
|
_("Install the $0 package."),
|
|
<span className="pf-v6-u-font-family-monospace-vf">kexec-tools</span>
|
|
);
|
|
}
|
|
}
|
|
return (
|
|
<Page className="pf-m-no-sidebar">
|
|
<PageSection hasBodyWrapper={false}>
|
|
<Flex spaceItems={{ default: 'spaceItemsMd' }} alignItems={{ default: 'alignItemsCenter' }}>
|
|
<Title headingLevel="h2" size="3xl">
|
|
{_("Kernel crash dump")}
|
|
</Title>
|
|
{kdumpSwitch}
|
|
{kdumpSwitchHelper &&
|
|
<HelperText className="subtle-helper-text">
|
|
<HelperTextItem>{kdumpSwitchHelper}</HelperTextItem>
|
|
</HelperText>}
|
|
{automationButton}
|
|
</Flex>
|
|
</PageSection>
|
|
<PageSection hasBodyWrapper={false}>
|
|
|
|
{alertMessage &&
|
|
<Alert variant='danger'
|
|
className="pf-v6-u-mb-md"
|
|
isLiveRegion={this.props.isLiveRegion}
|
|
isInline
|
|
title={alertMessage}>
|
|
{alertDetail}
|
|
</Alert>
|
|
}
|
|
<Card isPlain>
|
|
<CardTitle>
|
|
<Title headingLevel="h4" size="xl">
|
|
{_("Kdump settings")}
|
|
</Title>
|
|
</CardTitle>
|
|
<CardBody>
|
|
<DescriptionList className="pf-m-horizontal-on-sm">
|
|
<DescriptionListGroup>
|
|
<DescriptionListTerm>{_("Reserved memory")}</DescriptionListTerm>
|
|
<DescriptionListDescription>
|
|
{reservedMemory}
|
|
</DescriptionListDescription>
|
|
</DescriptionListGroup>
|
|
|
|
<DescriptionListGroup>
|
|
<DescriptionListTerm>{_("Crash dump location")}</DescriptionListTerm>
|
|
<DescriptionListDescription>
|
|
<Flex spaceItems={{ default: 'spaceItemsSm' }}>
|
|
<span id="kdump-target-info">{ kdumpLocation }</span>
|
|
{settingsLink}
|
|
</Flex>
|
|
</DescriptionListDescription>
|
|
</DescriptionListGroup>
|
|
|
|
<DescriptionListGroup>
|
|
<DescriptionListTerm />
|
|
<DescriptionListDescription>
|
|
{testButton}
|
|
</DescriptionListDescription>
|
|
</DescriptionListGroup>
|
|
</DescriptionList>
|
|
</CardBody>
|
|
</Card>
|
|
</PageSection>
|
|
</Page>
|
|
);
|
|
}
|
|
}
|