LCOV - code coverage report
Current view: top level - pkg/kdump - config-client-suse.js Coverage Total Hit
Test: cockpit Lines: 85.5 % 165 141
Test Date: 2026-07-17 07:33:01

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

Generated by: LCOV version 2.0-1