diff --git a/_service b/_service
index f804a83..0adac21 100644
--- a/_service
+++ b/_service
@@ -1,4 +1,14 @@
+
+ https://github.com/dgdavid/cockpit-suse-theme.git
+
+ git
+ main
+
+
+ cockpit-suse-theme.obsinfo
+
+
@PARENT_TAG@
http://github.com/cockpit-project/cockpit.git
@@ -17,4 +27,6 @@
cockpit
+
+
diff --git a/_servicedata b/_servicedata
index 5065ec3..4a037d0 100644
--- a/_servicedata
+++ b/_servicedata
@@ -1,4 +1,6 @@
http://github.com/cockpit-project/cockpit.git
- 046b3d4b381cd60f9d44756fc51f15ed7e17d0b5
\ No newline at end of file
+ 046b3d4b381cd60f9d44756fc51f15ed7e17d0b5
+ https://github.com/dgdavid/cockpit-suse-theme.git
+ fe08b35e64ad45f4b56cd636fdfec8fb18d69ab7
\ No newline at end of file
diff --git a/cockpit-suse-theme.obscpio b/cockpit-suse-theme.obscpio
new file mode 100644
index 0000000..757fc1d
--- /dev/null
+++ b/cockpit-suse-theme.obscpio
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c608d92eb3a695dd3148d2e01bf676429189755267826a061f44c7f2843877b4
+size 1275916
diff --git a/cockpit-suse-theme.obsinfo b/cockpit-suse-theme.obsinfo
new file mode 100644
index 0000000..1a37f47
--- /dev/null
+++ b/cockpit-suse-theme.obsinfo
@@ -0,0 +1,4 @@
+name: cockpit-suse-theme
+version:
+mtime: 1655379139
+commit: fe08b35e64ad45f4b56cd636fdfec8fb18d69ab7
diff --git a/cockpit.changes b/cockpit.changes
index 8e5cc5f..4a76e4e 100644
--- a/cockpit.changes
+++ b/cockpit.changes
@@ -1,3 +1,21 @@
+-------------------------------------------------------------------
+Tue Sep 13 09:46:17 UTC 2022 - Adam Majer
+
+- Fix cockpit-storage dependencies
+- Merge SUSE branding into cockpit package
+
+-------------------------------------------------------------------
+Mon Sep 5 08:09:56 UTC 2022 - Jacek Tomasiak
+
+- Update kdump-suse.patch to match upstream.
+
+-------------------------------------------------------------------
+Wed Aug 24 12:25:42 UTC 2022 - Jacek Tomasiak
+
+- 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
diff --git a/cockpit.spec b/cockpit.spec
index 0755d40..4ecc28d 100644
--- a/cockpit.spec
+++ b/cockpit.spec
@@ -55,6 +55,7 @@ Release: 0
Source0: cockpit-%{version}.tar
Source1: cockpit.pam
Source2: cockpit-rpmlintrc
+Source3: cockpit-suse-theme.tar
Source99: README.packaging
Source98: package-lock.json
Source97: node_modules.spec.inc
@@ -64,6 +65,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
@@ -179,12 +183,15 @@ Requires: subscription-manager-cockpit
%endif
%prep
-%setup -q -n cockpit-%{version}
+%setup -q -n cockpit-%{version} -a 3
%patch1 -p1
%patch2 -p1
%patch3 -p1
%patch4 -p1
%patch5 -p1
+%patch6 -p1
+%patch7 -p1
+%patch8 -p1
%if 0%{?sle_version}
%patch100 -p1
@@ -230,6 +237,13 @@ install -p -m 644 tools/cockpit.pam $RPM_BUILD_ROOT%{_sysconfdir}/pam.d/cockpit
rm -f %{buildroot}/%{_libdir}/cockpit/*.so
install -D -p -m 644 AUTHORS COPYING README.md %{buildroot}%{_docdir}/cockpit/
+mkdir -p %{buildroot}%{_datadir}/cockpit/branding/suse
+pushd cockpit-suse-theme
+cp src/css-overrides.css %{buildroot}%{_datadir}/cockpit/branding/suse
+cp src/fonts.css %{buildroot}%{_datadir}/cockpit/branding/suse
+cp -a src/fonts %{buildroot}%{_datadir}/cockpit/branding/suse
+popd
+
# only ship deprecated PatternFly API for stable releases
%if 0%{?rhel} == 8
if [ -f %{buildroot}/%{_datadir}/cockpit/base1/cockpit.css.gz ]; then
@@ -343,7 +357,7 @@ rm %{buildroot}/%{_prefix}/share/metainfo/org.cockpit-project.cockpit-selinux.me
# remove brandings with stale symlinks. Means they don't match
# the distro.
pushd %{buildroot}/%{_datadir}/cockpit/branding
-ls --hide={default,kubernetes,opensuse,registry,sle-micro} | xargs rm -rv
+ls --hide={default,kubernetes,opensuse,registry,sle-micro,suse} | xargs rm -rv
popd
# need this in SUSE as post build checks dislike stale symlinks
install -m 644 -D /dev/null %{buildroot}/run/cockpit/motd
@@ -697,14 +711,16 @@ Dummy package from building optional packages only; never install or publish me.
Summary: Cockpit user interface for storage, using udisks
Requires: cockpit-shell >= 266
Requires: udisks2 >= 2.9
+Requires: %{__python3}
+%if 0%{?suse_version}
+Requires: libudisks2-0_lvm2 >= 2.9
+Recommends: multipath-tools
+Requires: python3-dbus-python
+%else
Recommends: udisks2-lvm2 >= 2.9
Recommends: udisks2-iscsi >= 2.9
Recommends: device-mapper-multipath
Recommends: clevis-luks
-Requires: %{__python3}
-%if 0%{?suse_version}
-Requires: python3-dbus-python
-%else
Requires: python3-dbus
%endif
BuildArch: noarch
diff --git a/kdump-close.patch b/kdump-close.patch
new file mode 100644
index 0000000..ee0a8c5
--- /dev/null
+++ b/kdump-close.patch
@@ -0,0 +1,23 @@
+From 9be51b563c98744053e4a7412e5030fa2ab3e061 Mon Sep 17 00:00:00 2001
+From: Marius Vollmer
+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;
+ }
+ }
diff --git a/kdump-refactor.patch b/kdump-refactor.patch
new file mode 100644
index 0000000..72e4093
--- /dev/null
+++ b/kdump-refactor.patch
@@ -0,0 +1,712 @@
+From b61b273451987a283825a585a4f40901be12b11c Mon Sep 17 00:00:00 2001
+From: Jacek Tomasiak
+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 {
+
+ );
+ } 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 = (
+ <>
+-
+- this.props.onChange("nfs", value)} />
++
++ this.props.onChange("server", value)} />
++
++
++ this.props.onChange("export", value)} />
+
+
+
+ );
+ } 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 = (
+ <>
+
+ this.props.onChange("ssh", value)} />
++ placeholder="user@server.com" value={server}
++ onChange={value => this.props.onChange("server", value)} />
+
+
+
+@@ -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
+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
+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
+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];
+ }
diff --git a/kdump-suse.patch b/kdump-suse.patch
new file mode 100644
index 0000000..74efe23
--- /dev/null
+++ b/kdump-suse.patch
@@ -0,0 +1,491 @@
+From d95850239f81a65c90743f20a0bc0450cb61823a Mon Sep 17 00:00:00 2001
+From: Jacek Tomasiak
+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 .
++ */
++
++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
+ ?
+- : { kdumpLocation };
++ : { kdumpLocation };
+ 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")