Line data Source code
1 : /*
2 : * Copyright (C) 2022 SUSE LLC
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 : import { ConfigFile } from './config-client.js';
7 :
8 : /* Parse an dotenv-style config file
9 : * and monitor it for changes
10 : */
11 3 : export class ConfigFileSUSE extends ConfigFile {
12 : /* parse lines of the config file
13 : * if a line has a valid key=value format, use the key in _internal structure
14 : * and also store original line, line index, value and optional line suffix / comment
15 : * if value was quoted it will be stripped of quotes in `value` and `quoted` flag will
16 : * be used when writing the file to keep original formatting
17 : * e.g. for line 'someKey="foo" # comment'
18 : * outputObject._internal["someKey"] = {
19 : * index: 0,
20 : * value: "foo",
21 : * quoted: true,
22 : * origLine: 'someKey="foo" # comment',
23 : * suffix: "# comment"
24 : * }
25 : * skipNotify: Don't notify about changes, e.g.to avoid multiple updates when writing a file
26 : */
27 1 : _parseText(rawContent, skipNotify = false) {
28 1 : this._dataAvailableResolve();
29 :
30 : // clear settings if file is empty/missing
31 0 : if (!rawContent) {
32 0 : this._originalSettings = null;
33 0 : this.settings = null;
34 0 : if (!skipNotify)
35 0 : this.dispatchEvent("kdumpConfigChanged", this.settings);
36 0 : return;
37 0 : }
38 :
39 : // if nothing changed, don't bother parsing the content
40 1 : if (rawContent == this._rawContent)
41 1 : return;
42 :
43 1 : this._rawContent = rawContent;
44 :
45 : // this is the format expected by the UI
46 1 : this.settings = {
47 1 : _internal: {},
48 1 : targets: {},
49 1 : compression: { enabled: false, allowed: true },
50 1 : nfs_supports_directory: false,
51 1 : };
52 :
53 1 : this._lines = rawContent.split(/\r?\n/);
54 1 : this._lines.forEach((line, index) => {
55 1 : const trimmed = line.trim();
56 : // if the line is empty or only a comment, skip
57 1 : if (trimmed.indexOf("#") === 0 || trimmed.length === 0)
58 1 : return;
59 :
60 : // parse KEY=value or KEY="value" line
61 1 : let parts = trimmed.match(/^([A-Z_]+)\s*=\s*(.*)$/);
62 1 : if (parts === null) {
63 1 : console.warn("Malformed kdump config line:", trimmed, "in", this.filename);
64 1 : return;
65 1 : }
66 1 : const key = parts[1];
67 1 : let value = parts[2];
68 :
69 : // value might be quoted
70 1 : let quoted = false;
71 1 : if (value.startsWith('"')) {
72 1 : quoted = true;
73 1 : parts = value.match(/^"([^"]*)"\s*(.*)$/);
74 : // malformed line, no ending quote?
75 0 : if (parts === null) {
76 0 : console.warn("Incorrectly quoted value in kdump config line:", line, "in", this.filename);
77 0 : return;
78 0 : }
79 1 : } else {
80 : // not quoted should be simple value but grab everything and quote on write
81 1 : parts = value.match(/^([^#]+?)\s*(#.*)?$/);
82 1 : if (parts === null)
83 0 : parts = ["", ""];
84 1 : }
85 1 : value = parts[1];
86 1 : const suffix = (parts[2] || "").trim();
87 :
88 1 : this.settings._internal[key] = {
89 1 : index,
90 1 : value,
91 1 : origLine: line,
92 1 : quoted,
93 1 : suffix
94 1 : };
95 1 : });
96 :
97 : // make sure we copy the original keys so we overwrite the correct lines when saving
98 1 : this._originalSettings = { };
99 1 : Object.keys(this.settings._internal).forEach((key) => {
100 1 : this._originalSettings[key] = { ...this.settings._internal[key] };
101 1 : });
102 :
103 1 : this._extractSettings();
104 :
105 1 : if (!skipNotify)
106 1 : this.dispatchEvent("kdumpConfigChanged", this.settings);
107 1 : }
108 :
109 : /* extract settings managed by cockpit from _internal into platform independent model
110 : */
111 1 : _extractSettings() {
112 : // generate target(s) from KDUMP_SAVEDIR
113 1 : if ("KDUMP_SAVEDIR" in this.settings._internal && this.settings._internal.KDUMP_SAVEDIR.value) {
114 1 : let savedir = this.settings._internal.KDUMP_SAVEDIR.value;
115 : // handle legacy "file" without prefix
116 1 : if (savedir.startsWith("/"))
117 1 : savedir = "file://" + savedir;
118 : // server includes "username:password@" and can be empty for file://
119 1 : const parts = savedir.match(/^(.*):\/\/([^/]*)(\/.*)$/);
120 : // malformed KDUMP_SAVEDIR
121 1 : if (parts === null) {
122 1 : console.warn("Malformed KDUMP_SAVEDIR entry:", savedir, "in", this.filename);
123 1 : return;
124 1 : }
125 1 : const [, scheme, server, path] = parts;
126 1 : if (scheme === "file") {
127 1 : this.settings.targets.local = {
128 1 : type: "local",
129 1 : path,
130 1 : };
131 1 : } else if (scheme === "nfs") {
132 1 : this.settings.targets.nfs = {
133 1 : type: scheme,
134 : // on read full path is used as export
135 1 : export: path,
136 1 : server,
137 1 : };
138 1 : } else {
139 1 : this.settings.targets[scheme] = {
140 1 : type: scheme,
141 1 : path,
142 1 : server,
143 1 : };
144 : // sshkey is used by ssh and sftp/scp
145 1 : if ("KDUMP_SSH_IDENTITY" in this.settings._internal) {
146 1 : this.settings.targets[scheme].sshkey =
147 1 : this.settings._internal.KDUMP_SSH_IDENTITY.value;
148 1 : }
149 1 : }
150 1 : }
151 :
152 : // default to local if no target configured
153 1 : if (Object.keys(this.settings.targets).length === 0)
154 1 : this.settings.targets.local = { type: "local" };
155 :
156 1 : this.settings.compression.enabled = (
157 1 : !("KDUMP_DUMPFORMAT" in this.settings._internal) ||
158 : // TODO: what about other compression formats (lzo, snappy)?
159 1 : this.settings._internal.KDUMP_DUMPFORMAT.value === "compressed"
160 : );
161 1 : }
162 :
163 : /* update single _internal setting to given value
164 : * make sure setting exists if value is not empty
165 : * don't delete existing settings
166 : */
167 1 : _updateSetting(settings, key, value) {
168 1 : if (key in settings._internal) {
169 1 : settings._internal[key].value = value;
170 0 : } else {
171 0 : if (value)
172 0 : settings._internal[key] = { value };
173 0 : }
174 1 : }
175 :
176 : /* transform settings from model back to _internal format
177 : * this.settings = current state from file
178 : * settings = in-memory state from UI
179 : */
180 1 : _persistSettings(settings) {
181 : // target
182 1 : if (Object.keys(settings.targets).length > 0) {
183 1 : const target = Object.values(settings.targets)[0];
184 :
185 1 : if ("sshkey" in target)
186 1 : this._updateSetting(settings, "KDUMP_SSH_IDENTITY", target.sshkey);
187 :
188 1 : let savedir;
189 : // default for empty path (except nfs, see below)
190 1 : let path = target.path || "/var/crash";
191 1 : if (path && !path.startsWith("/"))
192 0 : path = "/" + path;
193 1 : if (target.type === "local") {
194 1 : savedir = "file://" + path;
195 1 : } else if (target.type === "nfs") {
196 : // override empty path default as nfs path is merged into export on read
197 1 : if (!target.path)
198 1 : path = "";
199 1 : let exprt = target.export;
200 1 : if (!exprt.startsWith("/"))
201 0 : exprt = "/" + exprt;
202 1 : savedir = "nfs://" + target.server + exprt + path;
203 1 : } else {
204 1 : savedir = target.type + "://" + target.server + path;
205 1 : }
206 1 : this._updateSetting(settings, "KDUMP_SAVEDIR", savedir);
207 1 : }
208 : // compression
209 1 : if (this.settings.compression.enabled != settings.compression.enabled) {
210 1 : if (settings.compression.enabled) {
211 1 : this._updateSetting(settings, "KDUMP_DUMPFORMAT", "compressed");
212 1 : } else {
213 1 : this._updateSetting(settings, "KDUMP_DUMPFORMAT", "ELF");
214 1 : }
215 1 : }
216 1 : return settings;
217 1 : }
218 :
219 : /* generate the config file from raw text and settings
220 : */
221 1 : generateConfig(settings) {
222 1 : settings = this._persistSettings(settings);
223 :
224 1 : const lines = this._lines.slice(0);
225 :
226 : // we take the lines from our last read operation and modify them with the new settings
227 1 : Object.keys(settings._internal).forEach((key) => {
228 1 : const entry = settings._internal[key];
229 :
230 0 : let value = entry.value !== undefined ? entry.value : "";
231 : // quote what was quoted before + empty values + multi-word values
232 1 : if (entry.quoted || value === "" || value.includes(" "))
233 1 : value = '"' + value + '"';
234 1 : let line = key + "=" + value;
235 1 : if (entry.suffix)
236 1 : line = line + " " + entry.suffix;
237 : // this might be a new entry
238 0 : if (!(key in this._originalSettings)) {
239 0 : lines.push(line);
240 0 : return;
241 0 : }
242 : // otherwise edit the old line
243 1 : const origEntry = this._originalSettings[key];
244 1 : lines[origEntry.index] = line;
245 1 : });
246 :
247 : // make sure file ends with a newline
248 1 : if (lines[lines.length - 1] !== "")
249 0 : lines.push("");
250 1 : return lines.join("\n");
251 1 : }
252 3 : }
|