/* * 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 = { error.details.replaceAll(/\nkdump: /g, "\n") }; setError(error); } else { // without a journal, show the error as-is setError(new Error(cockpit.format(_("Unable to save settings: $0"), String(error)))); } }); }; return ( {error && }
changeStorageLocation(val)} id="kdump-settings-location" value={storageLocation}> {storageLocation === "local" && setDirectory(value)} isRequired /> } {storageLocation === "nfs" && <> setServer(value)} isRequired /> setExportPath(value)} isRequired /> {settings.nfs_supports_directory && setDirectory(value)} isRequired /> } } {storageLocation === "ssh" && <> setServer(value)} isRequired /> changeSSHKey(value)} validated={validationErrors.sshkey ? "error" : "default"} /> setDirectory(value)} isRequired /> } setCompressionEnabled(c)} isDisabled={!compressionAllowed} label={_("Compress crash dumps to save space")} />
); }; 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 ( {error && } {_("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.")} {verifyMessage && {verifyMessage} } ); }; /* 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."), {path}, vmcore); } 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."), {target.type === "ssh" ? "SSH" : "NFS"}, {path}, vmcore); } } Dialogs.show(); } handleServiceDetailsClick() { cockpit.jump("/system/services#/kdump.service", cockpit.transport.host); } handleSettingsClick() { const Dialogs = this.context; Dialogs.show(); } 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( ); } render() { let kdumpLocation = (
{ _("Loading...") }
); 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 && ; let reservedMemory; if (this.props.reservedMemory === undefined) { // still waiting for result reservedMemory = (
{ _("Reading...") }
); } else if (this.props.reservedMemory === 0) { // nothing reserved reservedMemory = {_("None")} ; } else if (Number.isInteger(this.props.reservedMemory)) { // TODO: hint at using debug_mem_level to identify actual memory required? reservedMemory = {cockpit.format_bytes(this.props.reservedMemory, { base2: true })}; } 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 = ( { _("Test configuration") } ); } else { const tooltip = _("Test is only available while the kdump service is running."); testButton = ( ); } let automationButton = null; if (this.props.kdumpStatus && this.props.kdumpStatus.config !== null && this.state.os_release !== null && targetCanChange) { automationButton = ( ); } let kdumpSwitch; let kdumpSwitchHelper; if (!this.props.kdumpCmdlineEnabled) { kdumpSwitchHelper = _("Currently not supported"); } else { kdumpSwitch = (); } 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"), crashkernel ); 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."), crashkernel, crashkernel=512M, GRUB_CMDLINE_LINUX, /etc/default/grub ); } else if (this.props.kdumpStatus.state === "failed") { alertMessage = ( <> {_("Service has an error")} ); } } else { alertMessage = _("Kdump service is not installed."); alertDetail = fmt_to_fragments( _("Install the $0 package."), kexec-tools ); } } return ( {_("Kernel crash dump")} {kdumpSwitch} {kdumpSwitchHelper && {kdumpSwitchHelper} } {automationButton} {alertMessage && {alertDetail} } {_("Kdump settings")} {_("Reserved memory")} {reservedMemory} {_("Crash dump location")} { kdumpLocation } {settingsLink} {testButton} ); } }