/* * Copyright (C) 2016 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ import cockpit from 'cockpit'; import { proxy as serviceProxy } from 'service'; import { ConfigFile } from './config-client.js'; import { ConfigFileSUSE } from './config-client-suse.js'; import crashKernelScript from './crashkernel.sh'; import testWritableScript from './testwritable.sh'; const _ = cockpit.gettext; /* initializes the kdump status * emits "kdumpStatusChanged" when the status changes, along with a status object: * { * installed: true/false, * enabled: true/false, * state: current state, * config: settings from kdump.conf * target: dump target info, content depends on dump type * always contains the keys: * type value in ["local", "nfs", "ssh", "raw", "mount", "unknown"] * multipleTargets true if the config file has more than one target defined, false otherwise * } * */ export class KdumpClient { constructor() { this.state = { installed: undefined, enabled: undefined, state: undefined, config: undefined, target: undefined, }; cockpit.event_target(this); // listen to service status changes this.kdumpService = serviceProxy("kdump"); this.kdumpService.addEventListener('changed', () => { this.state.installed = this.kdumpService.exists; this.state.enabled = this.kdumpService.enabled; this.state.state = this.kdumpService.state; this.dispatchEvent("kdumpStatusChanged", this.state); }); // watch the config file this.configClient = new ConfigFile("/etc/kdump.conf", true); this._watchConfigChanges(); this.configClient.wait().then(() => { // if no configuration found, try SUSE version if (this.configClient.settings === null) { this.configClient.close(); this.configClient = new ConfigFileSUSE("/etc/sysconfig/kdump", true); this._watchConfigChanges(); } }); } _watchConfigChanges() { // catch config changes this.configClient.addEventListener('kdumpConfigChanged', () => { this.state.config = this.configClient.settings; this.state.target = this.targetFromSettings(this.configClient.settings); this.dispatchEvent("kdumpStatusChanged", this.state); }); } ensureOn() { // we consider the state to be "on" when it's enabled and running return Promise.all([ this.kdumpService.enable(), this.kdumpService.start() ]); } ensureOff() { // we consider the state to be "off" when it's disabled and stopped return Promise.all([ this.kdumpService.stop(), this.kdumpService.disable() ]); } crashKernel() { // crash the system kernel return cockpit.script(crashKernelScript, [], { superuser: "require" }); } validateSettings(settings) { const target = this.targetFromSettings(settings); let path; if (target && target.path) path = target.path; // if path is invalid or we haven't set one, use default if (!path) path = "/var/crash"; return new Promise((resolve, reject) => { if (target.type === "local") { // local path, try to see if we can write cockpit.script(testWritableScript, [path], { superuser: "try" }) .then(resolve) .catch(() => reject(cockpit.format(_("Directory $0 isn't writable or doesn't exist."), path))); return; } else if (target.type === "nfs") { if (!target.server || !target.server.trim()) reject(_("nfs server is empty")); // IPv6 must be enclosed in square brackets if (target.server.trim().match(/^\[.*[^\]]$/)) reject(_("nfs server is not valid IPv6")); if (!target.export || !target.export.trim()) reject(_("nfs export is empty")); } else if (target.type === "ssh") { if (!target.server || !target.server.trim()) reject(_("ssh server is empty")); if (target.sshkey && !target.sshkey.match("/.+")) reject(_("ssh key isn't a path")); } /* no-op if already rejected */ resolve(); }); } writeSettings(settings) { return this.configClient.write(settings) .then(() => { // after we've written the new config, we have to restart the service to pick up changes or clean up after errors if (this.kdumpService.enabled) { return this.kdumpService.restart() .catch(error => this.kdumpService.getRunJournal(["--output=cat", "--identifier=kdumpctl"]) .then(journal => { error.details = journal; return Promise.reject(error); }, ex => { console.warn("Failed to get journal of kdump.service:", ex.toString()); return Promise.reject(error); }) ); } else { return true; } }); } exportConfig(settings) { return this.configClient.generateConfig(settings).trim(); } targetFromSettings(settings) { const target = { type: "unknown", multipleTargets: false, }; if (!settings || Object.keys(settings.targets).length === 0) return target; // copy first target Object.assign(target, Object.values(settings.targets)[0]); target.multipleTargets = Object.keys(settings.targets).length > 1; return target; } }