cockpit/kdump-suse.patch
Adam Majer 7109a2d114 Accepting request 1001225 from home:jtomasiak:branches:systemsmanagement:cockpit
- Update kdump-suse.patch to match upstream.

- Add kdump-close.patch required by patches below.
- Add kdump-refactor.patch and kdump-suse.patch to support SUSE
  kdump config management in cockpit.

OBS-URL: https://build.opensuse.org/request/show/1001225
OBS-URL: https://build.opensuse.org/package/show/systemsmanagement:cockpit/cockpit?expand=0&rev=105
2022-09-13 08:59:38 +00:00

492 lines
21 KiB
Diff

From d95850239f81a65c90743f20a0bc0450cb61823a Mon Sep 17 00:00:00 2001
From: Jacek Tomasiak <jacek.tomasiak@gmail.com>
Date: Fri, 2 Sep 2022 16:51:02 +0200
Subject: [PATCH] kdump: Add SUSE kdump config support
If parsing /etc/kdump.conf doesn't return usable settings, try
/etc/sysconfig/kdump which is used by SUSE distributions as the main
kdump config.
The file is in dotenv format and currently only KDUMP_SAVEDIR,
KDUMP_DUMPFORMAT and KDUMP_SSH_IDENTITY entries
are used by Cockpit.
SUSE supports additional dump target types (ftp, sftp, cifs) but doesn't
support others (raw, mount). The target dialog currently supports the
common types (nfs, ssh, local).
---
pkg/kdump/config-client-suse.js | 265 ++++++++++++++++++++++++++++++++
pkg/kdump/kdump-client.js | 14 ++
pkg/kdump/kdump-view.jsx | 8 +-
test/verify/check-kdump | 127 +++++++++++++++
4 files changed, 413 insertions(+), 1 deletion(-)
create mode 100644 pkg/kdump/config-client-suse.js
diff --git a/pkg/kdump/config-client-suse.js b/pkg/kdump/config-client-suse.js
new file mode 100644
index 00000000000..074d9e406ca
--- /dev/null
+++ b/pkg/kdump/config-client-suse.js
@@ -0,0 +1,265 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2022 SUSE LLC
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import { ConfigFile } from './config-client.js';
+
+/* Parse an dotenv-style config file
+ * and monitor it for changes
+ */
+export class ConfigFileSUSE extends ConfigFile {
+ /* parse lines of the config file
+ * if a line has a valid key=value format, use the key in _internal structure
+ * and also store original line, line index, value and optional line suffix / comment
+ * if value was quoted it will be stripped of quotes in `value` and `quoted` flag will
+ * be used when writing the file to keep original formatting
+ * e.g. for line 'someKey="foo" # comment'
+ * outputObject._internal["someKey"] = {
+ * index: 0,
+ * value: "foo",
+ * quoted: true,
+ * origLine: 'someKey="foo" # comment',
+ * suffix: "# comment"
+ * }
+ * skipNotify: Don't notify about changes, e.g.to avoid multiple updates when writing a file
+ */
+ _parseText(rawContent, skipNotify = false) {
+ this._dataAvailableResolve();
+
+ // clear settings if file is empty/missing
+ if (!rawContent) {
+ this._originalSettings = null;
+ this.settings = null;
+ if (!skipNotify)
+ this.dispatchEvent("kdumpConfigChanged", this.settings);
+ return;
+ }
+
+ // if nothing changed, don't bother parsing the content
+ if (rawContent == this._rawContent)
+ return;
+
+ this._rawContent = rawContent;
+
+ // this is the format expected by the UI
+ this.settings = {
+ _internal: {},
+ targets: {},
+ compression: { enabled: false, allowed: true },
+ };
+
+ this._lines = rawContent.split(/\r?\n/);
+ this._lines.forEach((line, index) => {
+ const trimmed = line.trim();
+ // if the line is empty or only a comment, skip
+ if (trimmed.indexOf("#") === 0 || trimmed.length === 0)
+ return;
+
+ // parse KEY=value or KEY="value" line
+ let parts = trimmed.match(/^([A-Z_]+)\s*=\s*(.*)$/);
+ if (parts === null) {
+ console.warn("Malformed kdump config line:", trimmed, "in", this.filename);
+ return;
+ }
+ const key = parts[1];
+ let value = parts[2];
+
+ // value might be quoted
+ let quoted = false;
+ if (value.startsWith('"')) {
+ quoted = true;
+ parts = value.match(/^"([^"]*)"\s*(.*)$/);
+ // malformed line, no ending quote?
+ if (parts === null) {
+ console.warn("Incorrectly quoted value in kdump config line:", line, "in", this.filename);
+ return;
+ }
+ } else {
+ // not quoted should be simple value but grab everything and quote on write
+ parts = value.match(/^([^#]+?)\s*(#.*)?$/);
+ if (parts === null)
+ parts = ["", ""];
+ }
+ value = parts[1];
+ const suffix = (parts[2] || "").trim();
+
+ this.settings._internal[key] = {
+ index: index,
+ value: value,
+ origLine: line,
+ quoted: quoted,
+ suffix: suffix
+ };
+ });
+
+ // make sure we copy the original keys so we overwrite the correct lines when saving
+ this._originalSettings = { };
+ Object.keys(this.settings._internal).forEach((key) => {
+ this._originalSettings[key] = { ...this.settings._internal[key] };
+ });
+
+ this._extractSettings();
+
+ if (!skipNotify)
+ this.dispatchEvent("kdumpConfigChanged", this.settings);
+ }
+
+ /* extract settings managed by cockpit from _internal into platform independent model
+ */
+ _extractSettings() {
+ // generate target(s) from KDUMP_SAVEDIR
+ if ("KDUMP_SAVEDIR" in this.settings._internal && this.settings._internal.KDUMP_SAVEDIR.value) {
+ let savedir = this.settings._internal.KDUMP_SAVEDIR.value;
+ // handle legacy "file" without prefix
+ if (savedir.startsWith("/"))
+ savedir = "file://" + savedir;
+ // server includes "username:password@" and can be empty for file://
+ const parts = savedir.match(/^(.*):\/\/([^/]*)(\/.*)$/);
+ // malformed KDUMP_SAVEDIR
+ if (parts === null) {
+ console.warn("Malformed KDUMP_SAVEDIR entry:", savedir, "in", this.filename);
+ return;
+ }
+ const [, scheme, server, path] = parts;
+ if (scheme === "file") {
+ this.settings.targets.local = {
+ type: "local",
+ path: path,
+ };
+ } else if (scheme === "nfs") {
+ this.settings.targets.nfs = {
+ type: scheme,
+ // on read full path is used as export
+ export: path,
+ server: server,
+ };
+ } else {
+ this.settings.targets[scheme] = {
+ type: scheme,
+ path: path,
+ server: server,
+ };
+ // sshkey is used by ssh and sftp/scp
+ if ("KDUMP_SSH_IDENTITY" in this.settings._internal) {
+ this.settings.targets[scheme].sshkey =
+ this.settings._internal.KDUMP_SSH_IDENTITY.value;
+ }
+ }
+ }
+
+ // default to local if no target configured
+ if (Object.keys(this.settings.targets).length === 0)
+ this.settings.targets.local = { type: "local" };
+
+ this.settings.compression.enabled = (
+ !("KDUMP_DUMPFORMAT" in this.settings._internal) ||
+ // TODO: what about other compression formats (lzo, snappy)?
+ this.settings._internal.KDUMP_DUMPFORMAT.value === "compressed"
+ );
+ }
+
+ /* update single _internal setting to given value
+ * make sure setting exists if value is not empty
+ * don't delete existing settings
+ */
+ _updateSetting(settings, key, value) {
+ if (key in settings._internal) {
+ settings._internal[key].value = value;
+ } else {
+ if (value)
+ settings._internal[key] = { value: value };
+ }
+ }
+
+ /* transform settings from model back to _internal format
+ * this.settings = current state from file
+ * settings = in-memory state from UI
+ */
+ _persistSettings(settings) {
+ // target
+ if (Object.keys(settings.targets).length > 0) {
+ const target = Object.values(settings.targets)[0];
+
+ if ("sshkey" in target)
+ this._updateSetting(settings, "KDUMP_SSH_IDENTITY", target.sshkey);
+
+ let savedir;
+ // default for empty path (except nfs, see below)
+ let path = target.path || "/var/crash";
+ if (path && !path.startsWith("/"))
+ path = "/" + path;
+ if (target.type === "local") {
+ savedir = "file://" + path;
+ } else if (target.type === "nfs") {
+ // override empty path default as nfs path is merged into export on read
+ if (!target.path)
+ path = "";
+ let exprt = target.export;
+ if (!exprt.startsWith("/"))
+ exprt = "/" + exprt;
+ savedir = "nfs://" + target.server + exprt + path;
+ } else {
+ savedir = target.type + "://" + target.server + path;
+ }
+ this._updateSetting(settings, "KDUMP_SAVEDIR", savedir);
+ }
+ // compression
+ if (this.settings.compression.enabled != settings.compression.enabled) {
+ if (settings.compression.enabled) {
+ this._updateSetting(settings, "KDUMP_DUMPFORMAT", "compressed");
+ } else {
+ this._updateSetting(settings, "KDUMP_DUMPFORMAT", "ELF");
+ }
+ }
+ return settings;
+ }
+
+ /* generate the config file from raw text and settings
+ */
+ _generateConfig(settings) {
+ settings = this._persistSettings(settings);
+
+ const lines = this._lines.slice(0);
+
+ // we take the lines from our last read operation and modify them with the new settings
+ Object.keys(settings._internal).forEach((key) => {
+ const entry = settings._internal[key];
+
+ let value = entry.value !== undefined ? entry.value : "";
+ // quote what was quoted before + empty values + multi-word values
+ if (entry.quoted || value === "" || value.includes(" "))
+ value = '"' + value + '"';
+ let line = key + "=" + value;
+ if (entry.suffix)
+ line = line + " " + entry.suffix;
+ // this might be a new entry
+ if (!(key in this._originalSettings)) {
+ lines.push(line);
+ return;
+ }
+ // otherwise edit the old line
+ const origEntry = this._originalSettings[key];
+ lines[origEntry.index] = line;
+ });
+
+ // make sure file ends with a newline
+ if (lines[lines.length - 1] !== "")
+ lines.push("");
+ return lines.join("\n");
+ }
+}
diff --git a/pkg/kdump/kdump-client.js b/pkg/kdump/kdump-client.js
index d001ebb0b5a..7af24dc1bcb 100644
--- a/pkg/kdump/kdump-client.js
+++ b/pkg/kdump/kdump-client.js
@@ -20,6 +20,7 @@
import cockpit from 'cockpit';
import { proxy as serviceProxy } from 'service';
import { ConfigFile } from './config-client.js';
+import { ConfigFileSUSE } from './config-client-suse.js';
import crashKernelScript from 'raw-loader!./crashkernel.sh';
import testWritableScript from 'raw-loader!./testwritable.sh';
@@ -61,6 +62,19 @@ export class KdumpClient {
// watch the config file
this.configClient = new ConfigFile("/etc/kdump.conf", true);
+ this._watchConfigChanges();
+
+ this.configClient.wait().then(() => {
+ // if no configuration found, try SUSE version
+ if (this.configClient.settings === null) {
+ this.configClient.close();
+ this.configClient = new ConfigFileSUSE("/etc/sysconfig/kdump", true);
+ this._watchConfigChanges();
+ }
+ });
+ }
+
+ _watchConfigChanges() {
// catch config changes
this.configClient.addEventListener('kdumpConfigChanged', () => {
this.state.config = this.configClient.settings;
diff --git a/pkg/kdump/kdump-view.jsx b/pkg/kdump/kdump-view.jsx
index 956811d7826..3de3761706b 100644
--- a/pkg/kdump/kdump-view.jsx
+++ b/pkg/kdump/kdump-view.jsx
@@ -364,6 +364,12 @@ export class KdumpPage extends React.Component {
/* 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");
}
@@ -372,7 +378,7 @@ export class KdumpPage extends React.Component {
// this.storeLocation(this.props.kdumpStatus.config);
const settingsLink = targetCanChange
? <Button variant="link" isInline id="kdump-change-target" onClick={this.handleSettingsClick}>{ kdumpLocation }</Button>
- : <span>{ kdumpLocation }</span>;
+ : <span id="kdump-target-info">{ kdumpLocation }</span>;
let reservedMemory;
if (this.props.reservedMemory === undefined) {
// still waiting for result
diff --git a/test/verify/check-kdump b/test/verify/check-kdump
index 03d9a199970..855636eb0da 100755
--- a/test/verify/check-kdump
+++ b/test/verify/check-kdump
@@ -248,6 +248,133 @@ class TestKdump(KdumpHelpers):
conf = m.execute("cat /etc/kdump.conf")
self.assertIn(current + " -c", conf)
+ @nondestructive
+ def testConfigurationSUSE(self):
+ b = self.browser
+ m = self.machine
+
+ testConfig = [
+ "# some comment",
+ "KDUMP_DUMPFORMAT=compressed # suffix",
+ "KDUMP_SSH_IDENTITY=\"\"",
+ "skip this line",
+ "BAD_QUOTES=unquoted value # suffix",
+ "BAD_SPACES = 42 # comment",
+ "MORE_BAD_SPACES = 4 2 # long comment",
+ "KDUMP_SAVEDIR=ssh//missing/colon",
+ ]
+
+ # clean default config to trigger SUSE config mode
+ self.write_file("/etc/kdump.conf", "")
+ # write initial SUSE config (append to keep original contents as well)
+ self.write_file("/etc/sysconfig/kdump", "\n".join(testConfig), append=True)
+
+ m.execute("systemctl disable --now kdump")
+
+ self.login_and_go("/kdump")
+ b.wait_visible("#app")
+
+ # Check malformed lines
+ b.wait_text("#kdump-target-info", "No configuration found")
+ b.wait(lambda: "warning: Malformed kdump config line: skip this line in /etc/sysconfig/kdump" in list(self.browser.get_js_log()))
+ b.wait(lambda: "warning: Malformed KDUMP_SAVEDIR entry: ssh//missing/colon in /etc/sysconfig/kdump" in list(self.browser.get_js_log()))
+
+ # Remove malformed KDUMP_SAVEDIR to check default if nothing specified
+ m.execute("sed -i '/KDUMP_SAVEDIR=.*/d' /etc/sysconfig/kdump")
+ b.wait_text("#kdump-change-target", "locally in /var/crash")
+
+ # Check fixing of (some) malformed lines and local target without file://
+ m.execute("echo KDUMP_SAVEDIR=/tmp >> /etc/sysconfig/kdump")
+ b.wait_text("#kdump-change-target", "locally in /tmp")
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn('KDUMP_SAVEDIR=file:///tmp', conf)
+ self.assertIn('BAD_QUOTES="unquoted value" # suffix', conf)
+ self.assertIn('BAD_SPACES=42 # comment', conf)
+ self.assertIn('MORE_BAD_SPACES="4 2" # long comment', conf)
+
+ # Check remote ssh location
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_val("#kdump-settings-location", "ssh")
+ b.set_input_text("#kdump-settings-ssh-server", "admin@localhost")
+ b.set_input_text("#kdump-settings-ssh-key", "/home/admin/.ssh/id_rsa")
+ b.set_input_text("#kdump-settings-ssh-directory", "/var/tmp/crash")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ b.wait_text("#kdump-change-target", "Remote over SSH")
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn('KDUMP_SAVEDIR=ssh://admin@localhost/var/tmp/crash', conf)
+ self.assertIn('KDUMP_SSH_IDENTITY="/home/admin/.ssh/id_rsa"', conf)
+
+ # Check remote NFS location
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_val("#kdump-settings-location", "nfs")
+ b.set_input_text("#kdump-settings-nfs-server", "someserver")
+ b.set_input_text("#kdump-settings-nfs-export", "/srv")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ b.wait_text("#kdump-change-target", "Remote over NFS")
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn('KDUMP_SAVEDIR=nfs://someserver/srv', conf)
+ self.assertNotIn("ssh://", conf)
+
+ # NFS with custom path
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_input_text("#kdump-settings-nfs-directory", "dumps")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ b.wait_text("#kdump-change-target", "Remote over NFS")
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn('KDUMP_SAVEDIR=nfs://someserver/srv/dumps', conf)
+
+ # Check local location
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_val("#kdump-settings-location", "local")
+ b.set_input_text("#kdump-settings-local-directory", "/var/tmp")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ b.wait_text("#kdump-change-target", "locally in /var/tmp")
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn('KDUMP_SAVEDIR=file:///var/tmp', conf)
+ self.assertNotIn("nfs://", conf)
+
+ # Check compression
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn('KDUMP_DUMPFORMAT=compressed', conf)
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_checked("#kdump-settings-compression", False)
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn('KDUMP_DUMPFORMAT=ELF', conf)
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_checked("#kdump-settings-compression", True)
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn('KDUMP_DUMPFORMAT=compressed', conf)
+
+ # Check remote FTP location (no config dialog)
+ m.execute("sed -i 's/KDUMP_SAVEDIR=.*/KDUMP_SAVEDIR=ftp:\\/\\/user@ftpserver\\/dumps1/g' /etc/sysconfig/kdump")
+ b.wait_text("#kdump-target-info", "Remote over FTP")
+
+ # Check remote SFTP location (no config dialog)
+ m.execute("sed -i 's/KDUMP_SAVEDIR=.*/KDUMP_SAVEDIR=sftp:\\/\\/sftpserver\\/dumps2/g' /etc/sysconfig/kdump")
+ b.wait_text("#kdump-target-info", "Remote over SFTP")
+
+ # Check remote CIFS location (no config dialog)
+ m.execute("sed -i 's/KDUMP_SAVEDIR=.*/KDUMP_SAVEDIR=cifs:\\/\\/user:pass@smbserver\\/dumps3/g' /etc/sysconfig/kdump")
+ b.wait_text("#kdump-target-info", "Remote over CIFS/SMB")
+
@skipImage("kexec-tools not installed", "fedora-coreos", "debian-stable",
"debian-testing", "ubuntu-2204", "ubuntu-stable", "arch")