LCOV - code coverage report
Current view: top level - pkg/kdump - config-client.js Coverage Total Hit
Test: cockpit Lines: 81.6 % 239 195
Test Date: 2026-07-17 14:32:59

            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 : }
        

Generated by: LCOV version 2.0-1