mirror-cockpit-project-cockpit/pkg/kdump/config-client-suse.js
2026-10-16 17:49:20 +02:00

252 lines
9.2 KiB
JavaScript

/*
* Copyright (C) 2022 SUSE LLC
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
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 },
nfs_supports_directory: false,
};
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,
value,
origLine: line,
quoted,
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,
};
} else if (scheme === "nfs") {
this.settings.targets.nfs = {
type: scheme,
// on read full path is used as export
export: path,
server,
};
} else {
this.settings.targets[scheme] = {
type: scheme,
path,
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 };
}
}
/* 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");
}
}