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
This commit is contained in:
Adam Majer 2022-09-13 08:59:38 +00:00 committed by Git OBS Bridge
parent 05ccabd6e2
commit 7109a2d114
5 changed files with 1244 additions and 0 deletions

View File

@ -1,3 +1,15 @@
-------------------------------------------------------------------
Mon Sep 5 08:09:56 UTC 2022 - Jacek Tomasiak <jtomasiak@suse.com>
- Update kdump-suse.patch to match upstream.
-------------------------------------------------------------------
Wed Aug 24 12:25:42 UTC 2022 - Jacek Tomasiak <jtomasiak@suse.com>
- Add kdump-close.patch required by patches below.
- Add kdump-refactor.patch and kdump-suse.patch to support SUSE
kdump config management in cockpit.
-------------------------------------------------------------------
Wed Aug 24 07:37:46 UTC 2022 - Adam Majer <adam.majer@suse.de>

View File

@ -64,6 +64,9 @@ Patch2: hide-docs.patch
Patch3: suse-microos-branding.patch
Patch4: css-overrides.patch
Patch5: storage-btrfs.patch
Patch6: kdump-close.patch
Patch7: kdump-refactor.patch
Patch8: kdump-suse.patch
# SLE Micro specific patches
Patch100: remove-pwscore.patch
Patch101: hide-pcp.patch
@ -185,6 +188,9 @@ Requires: subscription-manager-cockpit
%patch3 -p1
%patch4 -p1
%patch5 -p1
%patch6 -p1
%patch7 -p1
%patch8 -p1
%if 0%{?sle_version}
%patch100 -p1

23
kdump-close.patch Normal file
View File

@ -0,0 +1,23 @@
From 9be51b563c98744053e4a7412e5030fa2ab3e061 Mon Sep 17 00:00:00 2001
From: Marius Vollmer <mvollmer@redhat.com>
Date: Mon, 8 Aug 2022 14:50:51 +0300
Subject: [PATCH] kdump: Use close with cockpit.file, not remove
There is no remove, and the intention is to close the watch channel.
---
pkg/kdump/config-client.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/kdump/config-client.js b/pkg/kdump/config-client.js
index 9b95b9a0c65..c58cb85fd99 100644
--- a/pkg/kdump/config-client.js
+++ b/pkg/kdump/config-client.js
@@ -47,7 +47,7 @@ export class ConfigFile {
close() {
if (this._fileHandle) {
- this._fileHandle.remove();
+ this._fileHandle.close();
this._fileHandle = undefined;
}
}

712
kdump-refactor.patch Normal file
View File

@ -0,0 +1,712 @@
From b61b273451987a283825a585a4f40901be12b11c Mon Sep 17 00:00:00 2001
From: Jacek Tomasiak <jacek.tomasiak@gmail.com>
Date: Tue, 12 Jul 2022 01:38:04 +0200
Subject: [PATCH 1/4] kdump: Refactor config handling
Config handling was modified to decouple UI from config file format.
All of the platform-specific code was moved to config-client.js and
"abstract" settings model was added as an interface.
In addition, NFS UI page was modified to not require NFS mounts in
specific format but as separate "server" and "export" fields.
---
pkg/kdump/config-client.js | 186 +++++++++++++++++++++++++++++++++++--
pkg/kdump/kdump-client.js | 87 ++++-------------
pkg/kdump/kdump-view.jsx | 125 ++++++++-----------------
3 files changed, 237 insertions(+), 161 deletions(-)
diff --git a/pkg/kdump/config-client.js b/pkg/kdump/config-client.js
index 39913bb6c86..d292bd9bebd 100644
--- a/pkg/kdump/config-client.js
+++ b/pkg/kdump/config-client.js
@@ -19,6 +19,12 @@
import cockpit from 'cockpit';
+const deprecatedKeys = ["net", "options", "link_delay", "disk_timeout", "debug_mem_level", "blacklist"];
+const knownKeys = [
+ "raw", "nfs", "ssh", "sshkey", "path", "core_collector", "kdump_post", "kdump_pre", "extra_bins", "extra_modules",
+ "default", "force_rebuild", "override_resettable", "dracut_args", "fence_kdump_args", "fence_kdump_nodes"
+];
+
/* Parse an ini-style config file
* and monitor it for changes
*/
@@ -82,7 +88,12 @@ export class ConfigFile {
// parse the config file
this._lines = rawContent.split(/\r?\n/);
- this.settings = { };
+ // this is the format expected by the UI
+ this.settings = {
+ _internal: {},
+ targets: {},
+ compression: { enabled: false, allowed: false, },
+ };
this._lines.forEach((line, index) => {
const trimmed = line.trim();
// if the line is empty or only a comment, skip
@@ -103,7 +114,7 @@ export class ConfigFile {
comment = value.substring(commentIndex).trim();
value = value.substring(0, commentIndex).trim();
}
- this.settings[key] = {
+ this.settings._internal[key] = {
index: index,
value: value,
origLine: line,
@@ -113,21 +124,182 @@ export class ConfigFile {
// make sure we copy the original keys so we overwrite the correct lines when saving
this._originalSettings = { };
- Object.keys(this.settings).forEach((key) => {
- this._originalSettings[key] = { ...this.settings[key] };
+ 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() {
+ // "path" applies to all targets
+ const path = this.settings._internal.path || { value: "" };
+
+ Object.keys(this.settings._internal).forEach((key) => {
+ if (key === "nfs") {
+ // split nfs line into server and export parts
+ const parts = this.settings._internal.nfs.value.match(/^([^[][^:]+|\[[^\]]+\]):(.*)$/);
+ if (!parts)
+ return;
+ this.settings.targets.nfs = {
+ type: key,
+ path: path.value,
+ server: parts[1],
+ export: parts[2],
+ };
+ } else if (key === "ssh") {
+ this.settings.targets.ssh = {
+ type: key,
+ path: path.value,
+ server: this.settings._internal.ssh.value,
+ };
+ if ("sshkey" in this.settings._internal)
+ this.settings.targets.ssh.sshkey = this.settings._internal.sshkey.value;
+ } else if (key === "raw") {
+ this.settings.targets.raw = {
+ type: key,
+ partition: this.settings._internal.raw.value
+ };
+ } else {
+ // probably local, but we might also have a mount
+ // check against known keys, the ones left over may be a mount target
+ // if the key is empty or known, we don't care about it here
+ if (!key || key in knownKeys || key in deprecatedKeys)
+ return;
+ // if we have a UUID, LABEL or /dev in the value, we can be pretty sure it's a mount option
+ const value = JSON.stringify(this.settings._internal[key]).toLowerCase();
+ if (value.indexOf("uuid") > -1 || value.indexOf("label") > -1 || value.indexOf("/dev") > -1) {
+ this.settings.targets.mount = {
+ type: "mount",
+ path: path.value,
+ fsType: key,
+ partition: this.settings._internal[key].value,
+ };
+ } else {
+ // TODO: check for know filesystem types here
+ }
+ }
+ });
+
+ // default to local if no target configured
+ if (Object.keys(this.settings.targets).length === 0)
+ this.settings.targets.local = { type: "local", path: path.value };
+
+ // only allow compression if there is no core collector set or it's set to makedumpfile
+ this.settings.compression.allowed = (
+ !("core_collector" in this.settings._internal) ||
+ (this.settings._internal.core_collector.value.trim().indexOf("makedumpfile") === 0)
+ );
+ // compression is enabled if we have a core_collector command with the "-c" parameter
+ this.settings.compression.enabled = (
+ ("core_collector" in this.settings._internal) &&
+ this.settings._internal.core_collector.value &&
+ (this.settings._internal.core_collector.value.split(" ").indexOf("-c") != -1)
+ );
+ }
+
+ /* update single _internal setting to given value
+ * make sure setting exists if value is not empty
+ */
+ _updateSetting(settings, key, value) {
+ if (key in settings._internal) {
+ if (value)
+ settings._internal[key].value = value;
+ else
+ delete settings._internal[key];
+ } 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];
+ this._updateSetting(settings, "path", target.path);
+
+ // wipe old target settings
+ for (const key in this.settings.targets) {
+ const oldTarget = this.settings.targets[key];
+ if (oldTarget.type == "mount") {
+ delete settings._internal[oldTarget.fsType];
+ } else {
+ delete settings._internal[key];
+ }
+ }
+
+ if (target.type === "nfs") {
+ this._updateSetting(settings, "nfs", [target.server, target.export].join(":"));
+ } else if (target.type === "ssh") {
+ this._updateSetting(settings, "ssh", target.server);
+ if ("sshkey" in target)
+ this._updateSetting(settings, "sshkey", target.sshkey);
+ } else if (target.type === "raw") {
+ this._updateSetting(settings, "raw", target.partition);
+ } else if (target.type === "mount") {
+ this._updateSetting(settings, target.fsType, target.partition);
+ }
+
+ /* ssh target needs a flattened vmcore for transport */
+ if ("core_collector" in settings._internal &&
+ settings._internal.core_collector.value.includes("makedumpfile")) {
+ if (target.type === "ssh" && !settings._internal.core_collector.value.includes("-F"))
+ settings._internal.core_collector.value += " -F";
+ else if (settings._internal.core_collector.value.includes("-F"))
+ settings._internal.core_collector.value =
+ settings._internal.core_collector.value
+ .split(" ")
+ .filter(e => e != "-F")
+ .join(" ");
+ }
+ }
+ // compression
+ if (this.settings.compression.enabled != settings.compression.enabled) {
+ if (settings.compression.enabled) {
+ // enable compression
+ if ("core_collector" in settings._internal)
+ settings._internal.core_collector.value = settings._internal.core_collector.value + " -c";
+ else
+ settings._internal.core_collector = { value: "makedumpfile -c" };
+ } else {
+ // disable compression
+ if ("core_collector" in this.settings._internal) {
+ // just remove all "-c" parameters
+ settings._internal.core_collector.value =
+ settings._internal.core_collector.value
+ .split(" ")
+ .filter((e) => { return (e != "-c") })
+ .join(" ");
+ } else {
+ // if we don't have anything on this in the original settings,
+ // we can get rid of the entry altogether
+ delete settings._internal.core_collector;
+ }
+ }
+ }
+ return settings;
+ }
+
/* generate the config file from raw text and settings
*/
_generateConfig(settings) {
+ settings = this._persistSettings(settings);
+
const lines = this._lines.slice(0);
const linesToDelete = [];
// first find the settings lines that have been disabled/deleted
Object.keys(this._originalSettings).forEach((key) => {
- if (!(key in settings) || !(key in settings && settings[key].value)) {
+ if (!(key in settings._internal) || !(key in settings._internal && settings._internal[key].value)) {
const origEntry = this._originalSettings[key];
// if the line had a comment, keep it, otherwise delete
if (origEntry.comment !== undefined)
@@ -138,8 +310,8 @@ export class ConfigFile {
});
// we take the lines from our last read operation and modify them with the new settings
- Object.keys(settings).forEach((key) => {
- const entry = settings[key];
+ Object.keys(settings._internal).forEach((key) => {
+ const entry = settings._internal[key];
let line = key + " " + entry.value;
if (entry.comment)
line = line + " " + entry.comment;
diff --git a/pkg/kdump/kdump-client.js b/pkg/kdump/kdump-client.js
index a161fc25214..d001ebb0b5a 100644
--- a/pkg/kdump/kdump-client.js
+++ b/pkg/kdump/kdump-client.js
@@ -25,12 +25,6 @@ import crashKernelScript from 'raw-loader!./crashkernel.sh';
import testWritableScript from 'raw-loader!./testwritable.sh';
const _ = cockpit.gettext;
-const deprecatedKeys = ["net", "options", "link_delay", "disk_timeout", "debug_mem_level", "blacklist"];
-const knownKeys = [
- "raw", "nfs", "ssh", "sshkey", "path", "core_collector", "kdump_post", "kdump_pre", "extra_bins", "extra_modules",
- "default", "force_rebuild", "override_resettable", "dracut_args", "fence_kdump_args", "fence_kdump_nodes"
-];
-
/* initializes the kdump status
* emits "kdumpStatusChanged" when the status changes, along with a status object:
* {
@@ -40,7 +34,7 @@ const knownKeys = [
* config: settings from kdump.conf
* target: dump target info, content depends on dump type
* always contains the keys:
- * target value in ["local", "nfs", "ssh", "raw", "mount", "unknown"]
+ * type value in ["local", "nfs", "ssh", "raw", "mount", "unknown"]
* multipleTargets true if the config file has more than one target defined, false otherwise
* }
*
@@ -106,19 +100,24 @@ export class KdumpClient {
path = "/var/crash";
return new Promise((resolve, reject) => {
- if (target.target === "local") {
+ if (target.type === "local") {
// local path, try to see if we can write
cockpit.script(testWritableScript, [path], { superuser: "try" })
.then(resolve)
.catch(() => reject(cockpit.format(_("Directory $0 isn't writable or doesn't exist."), path)));
return;
- } else if (target.target === "nfs") {
- if (!target.nfs.value.match("\\S+:/.+"))
- reject(_("nfs dump target isn't formatted as server:path"));
- } else if (target.target === "ssh") {
- if (!target.ssh.value.trim())
+ } else if (target.type === "nfs") {
+ if (!target.server || !target.server.trim())
+ reject(_("nfs server is empty"));
+ // IPv6 must be enclosed in square brackets
+ if (target.server.trim().match(/^\[.*[^\]]$/))
+ reject(_("nfs server is not valid IPv6"));
+ if (!target.export || !target.export.trim())
+ reject(_("nfs export is empty"));
+ } else if (target.type === "ssh") {
+ if (!target.server || !target.server.trim())
reject(_("ssh server is empty"));
- if (target.sshkey && !target.sshkey.value.match("/.+"))
+ if (target.sshkey && !target.sshkey.match("/.+"))
reject(_("ssh key isn't a path"));
}
@@ -149,67 +148,17 @@ export class KdumpClient {
}
targetFromSettings(settings) {
- // since local target is the default and can be used even without "path", we need to
- // check for the presence of all known targets
- // we have the additional difficulty that partitions don't have a good config key, since their
- // lines begin with the fs_type
const target = {
- target: "unknown",
+ type: "unknown",
multipleTargets: false,
};
- if (!settings)
+ if (!settings || Object.keys(settings.targets).length === 0)
return target;
- if ("nfs" in settings) {
- if (target.target != "unknown")
- target.multipleTargets = true;
- target.target = "nfs";
- target.nfs = settings.nfs;
- if ("path" in settings)
- target.path = settings.path;
- } else if ("ssh" in settings) {
- if (target.target != "unknown")
- target.multipleTargets = true;
- target.target = "ssh";
- target.ssh = settings.ssh;
- target.sshkey = settings.sshkey;
- } else if ("raw" in settings) {
- if (target.target != "unknown")
- target.multipleTargets = true;
- target.target = "raw";
- target.raw = settings.raw;
- } else {
- // probably local, but we might also have a mount
- // check all keys against known keys, the ones left over may be a mount target
- Object.keys(settings).forEach((key) => {
- // if the key is empty or known, we don't care about it here
- if (!key || key in knownKeys || key in deprecatedKeys)
- return;
- // if we have a UUID, LABEL or /dev in the value, we can be pretty sure it's a mount option
- const value = JSON.stringify(settings[key]).toLowerCase();
- if (value.indexOf("uuid") > -1 || value.indexOf("label") > -1 || value.indexOf("/dev") > -1) {
- if (target.target != "unknown")
- target.multipleTargets = true;
- target.target = "mount";
- target.fsType = key;
- target.partition = settings[key].value;
- } else {
- // TODO: check for know filesystem types here
- }
- });
- }
-
- // if no target matches, then we use the local filesystem
- if (target.target == "unknown")
- target.target = "local";
-
- // "path" applies to all targets
- // default to "/var/crash for "
- if ("path" in settings)
- target.path = settings.path.value;
- else if (["local", "ssh", "nfs", "mount"].indexOf(target.target) !== -1)
- target.path = "/var/crash";
+ // copy first target
+ cockpit.extend(target, Object.values(settings.targets)[0]);
+ target.multipleTargets = Object.keys(settings.targets).length > 1;
return target;
}
}
diff --git a/pkg/kdump/kdump-view.jsx b/pkg/kdump/kdump-view.jsx
index 718d8c43bc9..956811d7826 100644
--- a/pkg/kdump/kdump-view.jsx
+++ b/pkg/kdump/kdump-view.jsx
@@ -57,7 +57,7 @@ class KdumpTargetBody extends React.Component {
constructor(props) {
super(props);
this.state = {
- storeDest: this.props.initialTarget.target, // dialog mode, depends on location
+ storeDest: this.props.initialTarget.type, // dialog mode, depends on location
};
this.changeLocation = this.changeLocation.bind(this);
}
@@ -71,15 +71,10 @@ class KdumpTargetBody extends React.Component {
render() {
let detailRows;
- // only allow compression if there is no core collector set or it's set to makedumpfile
- const compressionPossible = (
- !this.props.settings ||
- !("core_collector" in this.props.settings) ||
- (this.props.settings.core_collector.value.trim().indexOf("makedumpfile") === 0)
- );
+ const compressionPossible = !this.props.settings || this.props.settings.compression.allowed;
let directory = "";
- if (this.props.settings && "path" in this.props.settings)
- directory = this.props.settings.path.value;
+ if (this.props.settings && "path" in this.props.settings.targets[this.state.storeDest])
+ directory = this.props.settings.targets[this.state.storeDest].path;
if (this.state.storeDest == "local") {
detailRows = (
@@ -91,15 +86,22 @@ class KdumpTargetBody extends React.Component {
</FormGroup>
);
} else if (this.state.storeDest == "nfs") {
- let nfs = "";
- if (this.props.settings && "nfs" in this.props.settings)
- nfs = this.props.settings.nfs.value;
+ let nfs = {};
+ if (this.props.settings && "nfs" in this.props.settings.targets)
+ nfs = this.props.settings.targets.nfs;
+ const server = nfs.server || "";
+ const exportpath = nfs.export || "";
detailRows = (
<>
- <FormGroup fieldId="kdump-settings-nfs-mount" label={_("Mount")}>
- <TextInput id="kdump-settings-nfs-mount" key="mount"
- placeholder="penguin.example.com:/export/cores" value={nfs}
- onChange={value => this.props.onChange("nfs", value)} />
+ <FormGroup fieldId="kdump-settings-nfs-server" label={_("Server")}>
+ <TextInput id="kdump-settings-nfs-server" key="server"
+ placeholder="penguin.example.com" value={server}
+ onChange={value => this.props.onChange("server", value)} />
+ </FormGroup>
+ <FormGroup fieldId="kdump-settings-nfs-export" label={_("Export")}>
+ <TextInput id="kdump-settings-nfs-export" key="export"
+ placeholder="/export/cores" value={exportpath}
+ onChange={value => this.props.onChange("export", value)} />
</FormGroup>
<FormGroup fieldId="kdump-settings-nfs-directory" label={_("Directory")}>
<TextInput id="kdump-settings-nfs-directory" key="directory"
@@ -110,18 +112,17 @@ class KdumpTargetBody extends React.Component {
</>
);
} else if (this.state.storeDest == "ssh") {
- let ssh = "";
- if (this.props.settings && "ssh" in this.props.settings)
- ssh = this.props.settings.ssh.value;
- let sshkey = "";
- if (this.props.settings && "sshkey" in this.props.settings)
- sshkey = this.props.settings.sshkey.value;
+ let ssh = {};
+ if (this.props.settings && "ssh" in this.props.settings.targets)
+ ssh = this.props.settings.targets.ssh;
+ const server = ssh.server || "";
+ const sshkey = ssh.sshkey || "";
detailRows = (
<>
<FormGroup fieldId="kdump-settings-ssh-server" label={_("Server")}>
<TextInput id="kdump-settings-ssh-server" key="server"
- placeholder="user@server.com" value={ssh}
- onChange={value => this.props.onChange("ssh", value)} />
+ placeholder="user@server.com" value={server}
+ onChange={value => this.props.onChange("server", value)} />
</FormGroup>
<FormGroup fieldId="kdump-settings-ssh-key" label={_("ssh key")}>
@@ -201,78 +202,32 @@ export class KdumpPage extends React.Component {
}
compressionStatus(settings) {
- // compression is enabled if we have a core_collector command with the "-c" parameter
- return (
- settings &&
- ("core_collector" in settings) &&
- settings.core_collector.value &&
- (settings.core_collector.value.split(" ").indexOf("-c") != -1)
- );
+ return settings && settings.compression.enabled;
}
changeSetting(key, value) {
let settings = this.state.dialogSettings;
- // a few special cases, otherwise write to config directly
+ // a few special cases, otherwise write to config target directly
if (key == "compression") {
- if (value) {
- // enable compression
- if ("core_collector" in settings)
- settings.core_collector.value = settings.core_collector.value + " -c";
- else
- settings.core_collector = { value: "makedumpfile -c" };
- } else {
- // disable compression
- if ("core_collector" in this.props.kdumpStatus.config) {
- // just remove all "-c" parameters
- settings.core_collector.value =
- settings.core_collector.value
- .split(" ")
- .filter((e) => { return (e != "-c") })
- .join(" ");
- } else {
- // if we don't have anything on this in the original settings,
- // we can get rid of the entry altogether
- delete settings.core_collector;
- }
- }
+ settings.compression.enabled = value;
} else if (key === "target") {
/* target changed, restore settings and wipe all settings associated
* with a target so no conflicting settings remain */
settings = {};
+ // TODO: do we need a deep copy here?
Object.keys(this.props.kdumpStatus.config).forEach((key) => {
settings[key] = { ...this.props.kdumpStatus.config[key] };
});
- Object.keys(this.props.kdumpStatus.target).forEach((key) => {
- if (settings[key])
- delete settings[key];
- });
- if (value === "ssh")
- settings.ssh = { value: "" };
- else if (value === "nfs")
- settings.nfs = { value: "" };
-
- if ("core_collector" in settings &&
- settings.core_collector.value.includes("makedumpfile")) {
- /* ssh target needs a flattened vmcore for transport */
- if (value === "ssh" && !settings.core_collector.value.includes("-F"))
- settings.core_collector.value += " -F";
- else if (settings.core_collector.value.includes("-F"))
- settings.core_collector.value =
- settings.core_collector.value
- .split(" ")
- .filter(e => e != "-F")
- .join(" ");
- }
+ settings.targets = {};
+ settings.targets[value] = { type: value };
} else if (key !== undefined) {
+ const type = Object.keys(settings.targets)[0];
if (!value) {
- if (settings[key])
- delete settings[key];
+ if (settings.targets[type][key])
+ delete settings.targets[type][key];
} else {
- if (key in settings)
- settings[key].value = value;
- else
- settings[key] = { value: value };
+ settings.targets[type][key] = value;
}
}
this.setState({ dialogSettings: settings });
@@ -391,21 +346,21 @@ export class KdumpPage extends React.Component {
if (target.multipleTargets) {
kdumpLocation = _("invalid: multiple targets defined");
} else {
- if (target.target == "local") {
+ if (target.type == "local") {
if (target.path)
kdumpLocation = cockpit.format(_("locally in $0"), target.path);
else
kdumpLocation = cockpit.format(_("locally in $0"), "/var/crash");
targetCanChange = true;
- } else if (target.target == "ssh") {
+ } else if (target.type == "ssh") {
kdumpLocation = _("Remote over SSH");
targetCanChange = true;
- } else if (target.target == "nfs") {
+ } else if (target.type == "nfs") {
kdumpLocation = _("Remote over NFS");
targetCanChange = true;
- } else if (target.target == "raw") {
+ } else if (target.type == "raw") {
kdumpLocation = _("Raw to a device");
- } else if (target.target == "mount") {
+ } else if (target.type == "mount") {
/* mount targets outside of nfs are too complex for the
* current target dialog */
kdumpLocation = _("On a mounted device");
From 2d045f916a9cff40e38411f8c07487d0b0f65b48 Mon Sep 17 00:00:00 2001
From: Jacek Tomasiak <jacek.tomasiak@gmail.com>
Date: Tue, 12 Jul 2022 12:03:28 +0200
Subject: [PATCH 2/4] kdump: Update config-client test
---
pkg/kdump/test-config-client.js | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/pkg/kdump/test-config-client.js b/pkg/kdump/test-config-client.js
index 61b10a57cb4..6eb84592d26 100644
--- a/pkg/kdump/test-config-client.js
+++ b/pkg/kdump/test-config-client.js
@@ -49,10 +49,10 @@ QUnit.test("config_update", function (assert) {
const dataWasChanged = new Promise(resolve => { this.dataWasChangedResolve = resolve });
let config;
const configChanged = (event, settings) => {
- assert.equal(settings.foo.value, "moo", "value changed correctly");
- assert.equal("key" in settings, false, "setting with comment deleted correctly");
- assert.equal("will" in settings, false, "setting without comment deleted correctly");
- assert.equal(settings.hooray.value, "value", "value added correctly");
+ assert.equal(settings._internal.foo.value, "moo", "value changed correctly");
+ assert.equal("key" in settings._internal, false, "setting with comment deleted correctly");
+ assert.equal("will" in settings._internal, false, "setting without comment deleted correctly");
+ assert.equal(settings._internal.hooray.value, "value", "value added correctly");
assert.equal(config._rawContent, changedConfig, "raw text for changed config is correct");
this.dataWasChangedResolve();
};
@@ -65,10 +65,10 @@ QUnit.test("config_update", function (assert) {
assert.equal(configFile.path, filename, "file has correct path");
config = new kdump.ConfigFile(filename);
config.wait().then(() => {
- config.settings.foo.value = "moo";
- delete config.settings.key;
- delete config.settings.will;
- config.settings.hooray = { value: "value" };
+ config.settings._internal.foo.value = "moo";
+ delete config.settings._internal.key;
+ delete config.settings._internal.will;
+ config.settings._internal.hooray = { value: "value" };
config.addEventListener('kdumpConfigChanged', configChanged);
config.write(config.settings)
.then(() => dataWasChanged.then(done));
From f94cf930e138681feeda272edef8172a5504cfb9 Mon Sep 17 00:00:00 2001
From: Jacek Tomasiak <jacek.tomasiak@gmail.com>
Date: Tue, 12 Jul 2022 12:21:59 +0200
Subject: [PATCH 3/4] kdump: Update kdump tests
---
test/verify/check-kdump | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/test/verify/check-kdump b/test/verify/check-kdump
index 5ef7108062c..107c6730dfa 100755
--- a/test/verify/check-kdump
+++ b/test/verify/check-kdump
@@ -109,15 +109,15 @@ class TestKdump(KdumpHelpers):
b.click("#kdump-change-target")
b.wait_visible("#kdump-settings-dialog")
b.set_val("#kdump-settings-location", "nfs")
- mountInput = "#kdump-settings-nfs-mount"
- b.set_input_text(mountInput, ":/var/crash")
+ serverInput = "#kdump-settings-nfs-server"
+ b.set_input_text(serverInput, "")
b.click(f"#kdump-settings-dialog button{self.primary_btn_class}")
- b.wait_in_text("#kdump-settings-dialog h4.pf-c-alert__title", "Unable to save settings: nfs dump target isn't formatted as server:path")
+ b.wait_in_text("#kdump-settings-dialog h4.pf-c-alert__title", "Unable to save settings: nfs server is empty")
# no further details/journal
self.assertFalse(b.is_present("#kdump-settings-dialog .pf-c-code-block__code"))
- b.set_input_text(mountInput, "localhost:")
+ b.set_input_text(serverInput, "localhost")
b.click(f"#kdump-settings-dialog button{self.primary_btn_class}")
- b.wait_in_text("#kdump-settings-dialog h4.pf-c-alert__title", "Unable to save settings: nfs dump target isn't formatted as server:path")
+ b.wait_in_text("#kdump-settings-dialog h4.pf-c-alert__title", "Unable to save settings: nfs export is empty")
b.click("#kdump-settings-dialog button.cancel")
b.wait_not_present("#kdump-settings-dialog")
@@ -207,7 +207,8 @@ class TestKdump(KdumpHelpers):
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-mount", "someserver:/srv")
+ 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")
conf = m.execute("cat /etc/kdump.conf")
@@ -277,7 +278,8 @@ class TestKdumpNFS(KdumpHelpers):
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-mount", "10.111.113.2:/srv/kdump")
+ b.set_input_text("#kdump-settings-nfs-server", "10.111.113.2")
+ b.set_input_text("#kdump-settings-nfs-export", "/srv/kdump")
b.click(f"#kdump-settings-dialog button{self.primary_btn_class}")
# rebuilding initrd might take a while on busy CI machines
with b.wait_timeout(300):
From 7a0d578063a1f4e25697eb13a9331f37d277857d Mon Sep 17 00:00:00 2001
From: Jacek Tomasiak <jacek.tomasiak@gmail.com>
Date: Tue, 19 Jul 2022 15:58:03 +0200
Subject: [PATCH 4/4] kdump: Fix ssh settings wiping
Leftover sshkey setting confused kdump on Fedora-35.
---
pkg/kdump/config-client.js | 3 +++
1 file changed, 3 insertions(+)
diff --git a/pkg/kdump/config-client.js b/pkg/kdump/config-client.js
index d292bd9bebd..9b95b9a0c65 100644
--- a/pkg/kdump/config-client.js
+++ b/pkg/kdump/config-client.js
@@ -233,6 +233,9 @@ export class ConfigFile {
const oldTarget = this.settings.targets[key];
if (oldTarget.type == "mount") {
delete settings._internal[oldTarget.fsType];
+ } else if (oldTarget.type == "ssh") {
+ delete settings._internal.ssh;
+ delete settings._internal.sshkey;
} else {
delete settings._internal[key];
}

491
kdump-suse.patch Normal file
View File

@ -0,0 +1,491 @@
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")