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 : import { proxy as serviceProxy } from 'service';
8 : import { ConfigFile } from './config-client.js';
9 : import { ConfigFileSUSE } from './config-client-suse.js';
10 :
11 : import crashKernelScript from './crashkernel.sh';
12 : import testWritableScript from './testwritable.sh';
13 3 : const _ = cockpit.gettext;
14 :
15 : /* initializes the kdump status
16 : * emits "kdumpStatusChanged" when the status changes, along with a status object:
17 : * {
18 : * installed: true/false,
19 : * enabled: true/false,
20 : * state: current state,
21 : * config: settings from kdump.conf
22 : * target: dump target info, content depends on dump type
23 : * always contains the keys:
24 : * type value in ["local", "nfs", "ssh", "raw", "mount", "unknown"]
25 : * multipleTargets true if the config file has more than one target defined, false otherwise
26 : * }
27 : *
28 : */
29 3 : export class KdumpClient {
30 3 : constructor() {
31 3 : this.state = {
32 3 : installed: undefined,
33 3 : enabled: undefined,
34 3 : state: undefined,
35 3 : config: undefined,
36 3 : target: undefined,
37 3 : };
38 3 : cockpit.event_target(this);
39 :
40 : // listen to service status changes
41 3 : this.kdumpService = serviceProxy("kdump");
42 3 : this.kdumpService.addEventListener('changed', () => {
43 3 : this.state.installed = this.kdumpService.exists;
44 3 : this.state.enabled = this.kdumpService.enabled;
45 3 : this.state.state = this.kdumpService.state;
46 3 : this.dispatchEvent("kdumpStatusChanged", this.state);
47 3 : });
48 :
49 : // watch the config file
50 3 : this.configClient = new ConfigFile("/etc/kdump.conf", true);
51 3 : this._watchConfigChanges();
52 :
53 3 : this.configClient.wait().then(() => {
54 : // if no configuration found, try SUSE version
55 1 : if (this.configClient.settings === null) {
56 1 : this.configClient.close();
57 1 : this.configClient = new ConfigFileSUSE("/etc/sysconfig/kdump", true);
58 1 : this._watchConfigChanges();
59 1 : }
60 3 : });
61 3 : }
62 :
63 3 : _watchConfigChanges() {
64 : // catch config changes
65 3 : this.configClient.addEventListener('kdumpConfigChanged', () => {
66 3 : this.state.config = this.configClient.settings;
67 3 : this.state.target = this.targetFromSettings(this.configClient.settings);
68 3 : this.dispatchEvent("kdumpStatusChanged", this.state);
69 3 : });
70 3 : }
71 :
72 0 : ensureOn() {
73 : // we consider the state to be "on" when it's enabled and running
74 0 : return Promise.all([
75 0 : this.kdumpService.enable(),
76 0 : this.kdumpService.start()
77 0 : ]);
78 0 : }
79 :
80 0 : ensureOff() {
81 : // we consider the state to be "off" when it's disabled and stopped
82 0 : return Promise.all([
83 0 : this.kdumpService.stop(),
84 0 : this.kdumpService.disable()
85 0 : ]);
86 0 : }
87 :
88 0 : crashKernel() {
89 : // crash the system kernel
90 0 : return cockpit.script(crashKernelScript, [], { superuser: "require" });
91 0 : }
92 :
93 2 : validateSettings(settings) {
94 2 : const target = this.targetFromSettings(settings);
95 2 : let path;
96 2 : if (target && target.path)
97 2 : path = target.path;
98 : // if path is invalid or we haven't set one, use default
99 2 : if (!path)
100 2 : path = "/var/crash";
101 :
102 2 : return new Promise((resolve, reject) => {
103 2 : if (target.type === "local") {
104 : // local path, try to see if we can write
105 2 : cockpit.script(testWritableScript, [path], { superuser: "try" })
106 2 : .then(resolve)
107 0 : .catch(() => reject(cockpit.format(_("Directory $0 isn't writable or doesn't exist."), path)));
108 2 : return;
109 2 : } else if (target.type === "nfs") {
110 2 : if (!target.server || !target.server.trim())
111 0 : reject(_("nfs server is empty"));
112 : // IPv6 must be enclosed in square brackets
113 2 : if (target.server.trim().match(/^\[.*[^\]]$/))
114 0 : reject(_("nfs server is not valid IPv6"));
115 2 : if (!target.export || !target.export.trim())
116 0 : reject(_("nfs export is empty"));
117 2 : } else if (target.type === "ssh") {
118 2 : if (!target.server || !target.server.trim())
119 0 : reject(_("ssh server is empty"));
120 2 : if (target.sshkey && !target.sshkey.match("/.+"))
121 0 : reject(_("ssh key isn't a path"));
122 2 : }
123 :
124 : /* no-op if already rejected */
125 2 : resolve();
126 2 : });
127 2 : }
128 :
129 2 : writeSettings(settings) {
130 2 : return this.configClient.write(settings)
131 2 : .then(() => {
132 : // after we've written the new config, we have to restart the service to pick up changes or clean up after errors
133 0 : if (this.kdumpService.enabled) {
134 0 : return this.kdumpService.restart()
135 0 : .catch(error => this.kdumpService.getRunJournal(["--output=cat", "--identifier=kdumpctl"])
136 0 : .then(journal => {
137 0 : error.details = journal;
138 0 : return Promise.reject(error);
139 0 : }, ex => {
140 0 : console.warn("Failed to get journal of kdump.service:", ex.toString());
141 0 : return Promise.reject(error);
142 0 : })
143 0 : );
144 0 : } else {
145 2 : return true;
146 2 : }
147 2 : });
148 2 : }
149 :
150 0 : exportConfig(settings) {
151 0 : return this.configClient.generateConfig(settings).trim();
152 0 : }
153 :
154 3 : targetFromSettings(settings) {
155 3 : const target = {
156 3 : type: "unknown",
157 3 : multipleTargets: false,
158 3 : };
159 :
160 3 : if (!settings || Object.keys(settings.targets).length === 0)
161 1 : return target;
162 :
163 : // copy first target
164 3 : Object.assign(target, Object.values(settings.targets)[0]);
165 3 : target.multipleTargets = Object.keys(settings.targets).length > 1;
166 3 : return target;
167 3 : }
168 3 : }
|