Line data Source code
1 : /*
2 : * Copyright (C) 2016 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 : import cockpit from 'cockpit';
7 :
8 3 : const deprecatedKeys = ["net", "options", "link_delay", "disk_timeout", "debug_mem_level", "blacklist"];
9 3 : const knownKeys = [
10 3 : "raw", "nfs", "ssh", "sshkey", "path", "core_collector", "kdump_post", "kdump_pre", "extra_bins", "extra_modules",
11 3 : "default", "force_rebuild", "override_resettable", "dracut_args", "fence_kdump_args", "fence_kdump_nodes"
12 3 : ];
13 : // man kdump.conf suggests this as default configuration
14 3 : const defaultCoreCollector = "makedumpfile -l --message-level 7 -d 31";
15 :
16 : /* Parse an ini-style config file
17 : * and monitor it for changes
18 : */
19 3 : export class ConfigFile {
20 3 : constructor(filename, superuser = false) {
21 3 : this.filename = filename;
22 3 : this._rawContent = undefined;
23 3 : this._lines = [];
24 3 : this._originalSettings = { };
25 3 : this._dataAvailable = new Promise(resolve => { this._dataAvailableResolve = resolve });
26 3 : this.settings = { };
27 :
28 3 : cockpit.event_target(this);
29 :
30 3 : this._fileHandle = cockpit.file(filename, { superuser });
31 3 : this._fileHandle.watch((rawContent) => {
32 3 : this._parseText(rawContent);
33 3 : });
34 3 : }
35 :
36 1 : close() {
37 1 : if (this._fileHandle) {
38 1 : this._fileHandle.close();
39 1 : this._fileHandle = undefined;
40 1 : }
41 1 : }
42 :
43 : // wait for data to have been read at least once
44 3 : wait() {
45 3 : return this._dataAvailable;
46 3 : }
47 :
48 : /* parse lines of the config file
49 : * if a line has a valid config key, use that as key
50 : * and also store original line, line index, value and whether the line contains a comment
51 : * we care about the comment since we don't want to overwrite a user comment when changing a value
52 : * e.g. for line "someKey foo # comment"
53 : * outputObject["someKey"] = { index: 0, value: "foo", origLine: "someKey foo # comment", hasComment: true }
54 : * skipNotify: Don't notify about changes, e.g.to avoid multiple updates when writing a file
55 : */
56 3 : _parseText(rawContent, skipNotify = false) {
57 3 : this._dataAvailableResolve();
58 :
59 : // if nothing changed, don't bother parsing the content
60 : // do proceed if the content is empty, it might be our initial read
61 1 : if (!rawContent) {
62 1 : this._originalSettings = null;
63 1 : this.settings = null;
64 1 : if (!skipNotify)
65 1 : this.dispatchEvent("kdumpConfigChanged", this.settings);
66 1 : return;
67 1 : }
68 :
69 2 : if (rawContent == this._rawContent)
70 2 : return;
71 :
72 : // if (skipNotify === undefined)
73 : // skipNotify = false;
74 :
75 2 : this._rawContent = rawContent;
76 : // parse the config file
77 2 : this._lines = rawContent.split(/\r?\n/);
78 :
79 : // this is the format expected by the UI
80 2 : this.settings = {
81 2 : _internal: {},
82 2 : targets: {},
83 2 : compression: { enabled: false, allowed: false, },
84 2 : nfs_supports_directory: true,
85 2 : };
86 2 : this._lines.forEach((line, index) => {
87 2 : const trimmed = line.trim();
88 : // if the line is empty or only a comment, skip
89 2 : if (trimmed.indexOf("#") === 0 || trimmed.length === 0)
90 2 : return;
91 :
92 : // we need to have a space between key and value
93 2 : const separatorIndex = trimmed.indexOf(" ");
94 2 : if (separatorIndex === -1)
95 2 : return;
96 2 : const key = trimmed.substring(0, separatorIndex);
97 2 : let value = trimmed.substring(separatorIndex + 1).trim();
98 :
99 : // value might have a comment at the end
100 2 : const commentIndex = value.indexOf("#");
101 2 : let comment;
102 0 : if (commentIndex !== -1) {
103 0 : comment = value.substring(commentIndex).trim();
104 0 : value = value.substring(0, commentIndex).trim();
105 0 : }
106 2 : this.settings._internal[key] = {
107 2 : index,
108 2 : value,
109 2 : origLine: line,
110 2 : comment
111 2 : };
112 2 : });
113 :
114 : // make sure we copy the original keys so we overwrite the correct lines when saving
115 2 : this._originalSettings = { };
116 2 : Object.keys(this.settings._internal).forEach((key) => {
117 2 : this._originalSettings[key] = { ...this.settings._internal[key] };
118 2 : });
119 :
120 2 : this._extractSettings();
121 :
122 2 : if (!skipNotify)
123 2 : this.dispatchEvent("kdumpConfigChanged", this.settings);
124 3 : }
125 :
126 : /* extract settings managed by cockpit from _internal into platform independent model
127 : */
128 2 : _extractSettings() {
129 : // "path" applies to all targets
130 1 : const path = this.settings._internal.path || { value: "" };
131 :
132 2 : Object.keys(this.settings._internal).forEach((key) => {
133 1 : if (key === "nfs") {
134 : // split nfs line into server and export parts
135 1 : const parts = this.settings._internal.nfs.value.match(/^([^[][^:]+|\[[^\]]+\]):(.*)$/);
136 1 : if (!parts)
137 1 : return;
138 1 : this.settings.targets.nfs = {
139 1 : type: key,
140 1 : path: path.value,
141 1 : server: parts[1],
142 1 : export: parts[2],
143 1 : };
144 1 : } else if (key === "ssh") {
145 1 : this.settings.targets.ssh = {
146 1 : type: key,
147 1 : path: path.value,
148 1 : server: this.settings._internal.ssh.value,
149 1 : };
150 1 : if ("sshkey" in this.settings._internal)
151 1 : this.settings.targets.ssh.sshkey = this.settings._internal.sshkey.value;
152 0 : } else if (key === "raw") {
153 0 : this.settings.targets.raw = {
154 0 : type: key,
155 0 : partition: this.settings._internal.raw.value
156 0 : };
157 0 : } else {
158 : // probably local, but we might also have a mount
159 : // check against known keys, the ones left over may be a mount target
160 : // if the key is empty or known, we don't care about it here
161 2 : if (!key || key in knownKeys || key in deprecatedKeys)
162 2 : return;
163 : // if we have a UUID, LABEL or /dev in the value, we can be pretty sure it's a mount option
164 2 : const value = JSON.stringify(this.settings._internal[key]).toLowerCase();
165 0 : if (value.indexOf("uuid") > -1 || value.indexOf("label") > -1 || value.indexOf("/dev") > -1) {
166 0 : this.settings.targets.mount = {
167 0 : type: "mount",
168 0 : path: path.value,
169 0 : fsType: key,
170 0 : partition: this.settings._internal[key].value,
171 0 : };
172 0 : } else {
173 : // TODO: check for know filesystem types here
174 2 : }
175 2 : }
176 2 : });
177 :
178 : // default to local if no target configured
179 2 : if (Object.keys(this.settings.targets).length === 0)
180 2 : this.settings.targets.local = { type: "local", path: path.value };
181 :
182 : // only allow compression if there is no core collector set or it's set to makedumpfile
183 2 : this.settings.compression.allowed = (
184 2 : !("core_collector" in this.settings._internal) ||
185 2 : (this.settings._internal.core_collector.value.trim().indexOf("makedumpfile") === 0)
186 : );
187 : // compression is enabled if we have a core_collector command with the "-c" parameter
188 2 : this.settings.compression.enabled = (
189 2 : ("core_collector" in this.settings._internal) &&
190 2 : this.settings._internal.core_collector.value &&
191 2 : (this.settings._internal.core_collector.value.split(" ").indexOf("-c") != -1)
192 : );
193 :
194 1 : this.settings.core_collector = this.settings._internal?.core_collector?.value ?? defaultCoreCollector;
195 2 : }
196 :
197 : /* update single _internal setting to given value
198 : * make sure setting exists if value is not empty
199 : */
200 1 : _updateSetting(settings, key, value) {
201 1 : if (key in settings._internal) {
202 1 : if (value)
203 1 : settings._internal[key].value = value;
204 : else
205 1 : delete settings._internal[key];
206 1 : } else {
207 1 : if (value)
208 1 : settings._internal[key] = { value };
209 1 : }
210 1 : }
211 :
212 : /* transform settings from model back to _internal format
213 : * this.settings = current state from file
214 : * settings = in-memory state from UI
215 : */
216 1 : _persistSettings(settings) {
217 : // target
218 1 : if (Object.keys(settings.targets).length > 0) {
219 1 : const target = Object.values(settings.targets)[0];
220 1 : this._updateSetting(settings, "path", target.path);
221 :
222 : // wipe old target settings
223 1 : for (const key in this.settings.targets) {
224 1 : const oldTarget = this.settings.targets[key];
225 0 : if (oldTarget.type == "mount") {
226 0 : delete settings._internal[oldTarget.fsType];
227 0 : } else if (oldTarget.type == "ssh") {
228 1 : delete settings._internal.ssh;
229 1 : delete settings._internal.sshkey;
230 1 : } else {
231 1 : delete settings._internal[key];
232 1 : }
233 1 : }
234 :
235 1 : if (target.type === "nfs") {
236 1 : this._updateSetting(settings, "nfs", [target.server, target.export].join(":"));
237 1 : } else if (target.type === "ssh") {
238 1 : this._updateSetting(settings, "ssh", target.server);
239 1 : if ("sshkey" in target)
240 1 : this._updateSetting(settings, "sshkey", target.sshkey);
241 0 : } else if (target.type === "raw") {
242 0 : this._updateSetting(settings, "raw", target.partition);
243 0 : } else if (target.type === "mount") {
244 0 : this._updateSetting(settings, target.fsType, target.partition);
245 0 : }
246 :
247 : /* ssh target needs a flattened vmcore for transport */
248 1 : if ("core_collector" in settings._internal &&
249 1 : settings._internal.core_collector.value.includes("makedumpfile")) {
250 1 : if (target.type === "ssh" && !settings._internal.core_collector.value.includes("-F"))
251 1 : settings._internal.core_collector.value += " -F";
252 1 : else if (settings._internal.core_collector.value.includes("-F"))
253 1 : settings._internal.core_collector.value =
254 1 : settings._internal.core_collector.value
255 1 : .split(" ")
256 1 : .filter(e => e != "-F")
257 1 : .join(" ");
258 1 : } else {
259 1 : settings._internal.core_collector = { value: defaultCoreCollector };
260 0 : if (target.type === "ssh") {
261 0 : settings._internal.core_collector.value += " -F";
262 0 : }
263 1 : }
264 1 : }
265 : // compression
266 1 : if (this.settings.compression.enabled != settings.compression.enabled) {
267 1 : if (settings.compression.enabled) {
268 : // enable compression
269 1 : if ("core_collector" in settings._internal)
270 0 : settings._internal.core_collector.value = settings._internal.core_collector.value + " -c";
271 : else
272 0 : settings._internal.core_collector = { value: defaultCoreCollector };
273 0 : } else {
274 : // disable compression
275 0 : if ("core_collector" in this.settings._internal) {
276 : // just remove all "-c" parameters
277 0 : settings._internal.core_collector.value =
278 0 : settings._internal.core_collector.value
279 0 : .split(" ")
280 0 : .filter((e) => { return (e != "-c") })
281 0 : .join(" ");
282 0 : } else {
283 : // if we don't have anything on this in the original settings,
284 : // we can get rid of the entry altogether
285 0 : delete settings._internal.core_collector;
286 0 : }
287 0 : }
288 1 : }
289 1 : return settings;
290 1 : }
291 :
292 : /* generate the config file from raw text and settings
293 : */
294 1 : generateConfig(settings) {
295 1 : settings = this._persistSettings(settings);
296 :
297 1 : const lines = this._lines.slice(0);
298 1 : const linesToDelete = [];
299 : // first find the settings lines that have been disabled/deleted
300 1 : Object.keys(this._originalSettings).forEach((key) => {
301 1 : if (!(key in settings._internal) || !(key in settings._internal && settings._internal[key].value)) {
302 1 : const origEntry = this._originalSettings[key];
303 : // if the line had a comment, keep it, otherwise delete
304 1 : if (origEntry.comment !== undefined)
305 0 : lines[origEntry.index] = "#" + origEntry.origLine;
306 : else
307 1 : linesToDelete.push(origEntry.index);
308 1 : }
309 1 : });
310 :
311 : // we take the lines from our last read operation and modify them with the new settings
312 1 : Object.keys(settings._internal).forEach((key) => {
313 1 : const entry = settings._internal[key];
314 1 : let line = key + " " + entry.value;
315 1 : if (entry.comment)
316 0 : line = line + " " + entry.comment;
317 : // this might be a new entry
318 1 : if (!(key in this._originalSettings)) {
319 1 : lines.push(line);
320 1 : return;
321 1 : }
322 : // otherwise edit the old line
323 1 : const origEntry = this._originalSettings[key];
324 1 : lines[origEntry.index] = line;
325 1 : });
326 : // now delete the rows we want to delete
327 1 : linesToDelete.sort().reverse()
328 1 : .forEach((lineIndex) => {
329 1 : lines.splice(lineIndex, 1);
330 1 : });
331 :
332 1 : return lines.join("\n") + "\n";
333 1 : }
334 :
335 : /* write settings back to file
336 : * new settings that don't have a corresponding entry already have an undefined or null index
337 : * returns a promise for the file operation (cockpit File)
338 : */
339 2 : write(settings) {
340 2 : return this._fileHandle.modify((oldContent) => {
341 2 : this._parseText(oldContent, true);
342 2 : return this.generateConfig(settings);
343 2 : });
344 2 : }
345 3 : }
|