Line data Source code
1 : /*
2 : * Copyright (C) 2016 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 2 : import cockpit from "cockpit";
7 : import 'cockpit-dark-theme'; // once per page
8 :
9 2 : import React from "react";
10 2 : import { createRoot } from "react-dom/client";
11 :
12 : import '../lib/patternfly/patternfly-6-cockpit.scss';
13 :
14 : import * as troubleshootClient from "./setroubleshoot-client.js";
15 : import * as selinuxClient from "./selinux-client.js";
16 : import { SETroubleshootPage } from "./setroubleshoot-view.jsx";
17 : import { superuser } from 'superuser';
18 :
19 : import "./setroubleshoot.scss";
20 :
21 2 : const _ = cockpit.gettext;
22 :
23 2 : superuser.reload_page_on_change();
24 :
25 2 : const initStore = function(rootElement) {
26 2 : const dataStore = { };
27 2 : dataStore.domRootElement = rootElement;
28 :
29 2 : dataStore.entries = [];
30 :
31 : // connected to the dbus api of setroubleshootd
32 2 : dataStore.connected = false;
33 :
34 : // currently trying to connect / used for timer
35 2 : dataStore.connecting = null;
36 :
37 : // did we have a connection error?
38 2 : dataStore.error = null;
39 :
40 2 : dataStore.client = troubleshootClient.client;
41 :
42 2 : dataStore.selinuxStatusError = undefined;
43 :
44 2 : const selinuxStatusChanged = function(status, errorMessage) {
45 2 : dataStore.selinuxStatus = status;
46 2 : if (errorMessage !== undefined)
47 1 : dataStore.selinuxStatusError = errorMessage;
48 2 : dataStore.render();
49 2 : };
50 0 : const selinuxStatusDismissError = function() {
51 0 : dataStore.selinuxStatusError = undefined;
52 0 : dataStore.render();
53 0 : };
54 1 : const selinuxChangeMode = function(_event, newMode) {
55 1 : selinuxClient.setEnforcing(newMode).then(
56 1 : function() {
57 1 : dataStore.selinuxStatus.enforcing = newMode;
58 1 : dataStore.render();
59 1 : },
60 0 : function(error) {
61 0 : dataStore.selinuxStatusError = cockpit.format(_("Error while setting SELinux mode: '$0'"), error.message);
62 0 : dataStore.render();
63 0 : }
64 1 : );
65 1 : };
66 2 : dataStore.selinuxStatus = selinuxClient.init(selinuxStatusChanged);
67 :
68 : // run a fix and update the entries accordingly
69 0 : const runFix = function(alertId, analysisId, fixId, runCommand) {
70 0 : let idx;
71 0 : for (idx = dataStore.entries.length - 1; idx >= 0; --idx) {
72 0 : if (dataStore.entries[idx].key == alertId)
73 0 : break;
74 0 : }
75 0 : if (idx < 0) {
76 0 : console.log("Unable to find alert entry for element requesting fix: " + alertId + " (" + analysisId + ").");
77 0 : return;
78 0 : }
79 0 : dataStore.entries[idx].details.pluginAnalysis[fixId].fix = {
80 0 : plugin: analysisId,
81 0 : running: true,
82 0 : result: null,
83 0 : success: false,
84 0 : };
85 0 : dataStore.render();
86 0 : const promise = runCommand
87 0 : ? cockpit.script(runCommand, { err: "message", superuser: "require" })
88 0 : : dataStore.client.runFix(alertId, analysisId);
89 :
90 0 : promise
91 0 : .then(output => {
92 0 : dataStore.entries[idx].details.pluginAnalysis[fixId].fix = {
93 0 : plugin: analysisId,
94 0 : running: false,
95 0 : result: output,
96 0 : success: true,
97 0 : };
98 0 : selinuxClient.getModifications(selinuxStatusChanged);
99 0 : dataStore.render();
100 0 : })
101 0 : .catch(error => {
102 0 : dataStore.entries[idx].details.pluginAnalysis[fixId].fix = {
103 0 : plugin: analysisId,
104 0 : running: false,
105 0 : result: error.toString(),
106 0 : success: false,
107 0 : };
108 0 : dataStore.render();
109 0 : });
110 0 : };
111 :
112 : /* Delete an alert via the client
113 : * if it goes wrong, show an error
114 : * remove the entry if successful
115 : * This function will only be called if the backend functionality is actually present
116 : */
117 0 : const deleteAlert = function(alertId) {
118 0 : return dataStore.client.deleteAlert(alertId)
119 0 : .then(() => {
120 0 : let idx;
121 0 : for (idx = dataStore.entries.length - 1; idx >= 0; --idx) {
122 0 : if (dataStore.entries[idx].key == alertId)
123 0 : break;
124 0 : }
125 0 : if (idx < 0)
126 0 : return;
127 0 : dataStore.entries.splice(idx, 1);
128 0 : dataStore.render();
129 0 : })
130 0 : .catch(error => {
131 0 : dataStore.error = error.toString();
132 0 : dataStore.render();
133 0 : });
134 0 : };
135 :
136 0 : const dismissError = function() {
137 0 : dataStore.error = null;
138 0 : dataStore.render();
139 0 : };
140 :
141 2 : const render = function() {
142 2 : if (!dataStore.reactRoot)
143 2 : dataStore.reactRoot = createRoot(rootElement);
144 :
145 2 : dataStore.reactRoot.render(React.createElement(SETroubleshootPage, {
146 2 : connected: dataStore.connected,
147 2 : connecting: dataStore.connecting,
148 2 : error: dataStore.error,
149 2 : dismissError,
150 2 : entries: dataStore.entries,
151 2 : runFix,
152 2 : deleteAlert,
153 2 : selinuxStatus: dataStore.selinuxStatus,
154 2 : selinuxStatusError: dataStore.selinuxStatusError,
155 2 : changeSelinuxMode: selinuxChangeMode,
156 2 : dismissStatusError: selinuxStatusDismissError,
157 2 : }));
158 2 : };
159 2 : dataStore.render = render;
160 :
161 : /* Update an alert entry if it exists, otherwise create one
162 : Details: if undefined, we don't have info on them yet,
163 : while null means an error occurred while retrieving them
164 : The function doesn't trigger a render
165 : */
166 2 : const maybeUpdateAlert = function(localId, description, count, details) {
167 : // if we already know about this alert, ignore unless the repetition count changed
168 : // we start at the back because that's where we push new entries
169 : // if we receive an alert multiple times, this is where it will be
170 2 : for (let idx = dataStore.entries.length - 1; idx >= 0; --idx) {
171 2 : if (dataStore.entries[idx].key == localId) {
172 1 : if (description === undefined || count === undefined) {
173 1 : dataStore.entries[idx].details = details;
174 1 : return;
175 1 : }
176 : // don't update newer information
177 : // this can happen in cases of highly frequent updates
178 2 : if (dataStore.entries[idx].count <= count) {
179 : // don't tamper with the status of a fix being run
180 : // new alerts might be coming in while a fix is running and we don't want
181 : // to lose the progress or result
182 :
183 : // only allow details to be null if the count has increased
184 1 : if ((details !== undefined) || (dataStore.entries[idx].count < count)) {
185 2 : dataStore.entries[idx].details = details;
186 2 : }
187 2 : dataStore.entries[idx].description = description;
188 2 : dataStore.entries[idx].count = count;
189 2 : }
190 2 : return;
191 2 : }
192 2 : }
193 : // nothing found, so we create a new entry
194 2 : dataStore.entries.push({ key: localId, description, count, details, fix: null });
195 2 : };
196 :
197 : /* Add a list of messages and triggers getting details for each of them
198 : The list is added without details at first (if it's a new entry) to preserve the order
199 : */
200 2 : const handleMultipleMessages = function(entries) {
201 2 : const detailPromises = [];
202 2 : for (let idxEntry = 0; idxEntry != entries.length; ++idxEntry) {
203 2 : const entry = entries[idxEntry];
204 2 : maybeUpdateAlert(entry.localId, entry.summary, entry.reportCount, undefined);
205 2 : detailPromises.push(dataStore.getAlertDetails(entry.localId));
206 2 : }
207 :
208 2 : Promise.all(detailPromises).then(render);
209 2 : };
210 :
211 0 : dataStore.handleAlert = function(level, localId) {
212 : // right now the level is unused, since we can't access it for existing alerts
213 :
214 : // we receive the item details in added delayed fashion, render only once we have the full info
215 0 : dataStore.getAlertDetails(localId);
216 0 : render();
217 0 : };
218 :
219 2 : const getAlertDetails = function(id) {
220 2 : return dataStore.client.getAlert(id)
221 2 : .then(details => {
222 2 : maybeUpdateAlert(id, details.summary, details.reportCount, details);
223 2 : })
224 0 : .catch(() => {
225 0 : maybeUpdateAlert(id, undefined, undefined, null);
226 0 : });
227 2 : };
228 2 : dataStore.getAlertDetails = getAlertDetails;
229 :
230 0 : const setDisconnected = function() {
231 0 : dataStore.connected = false;
232 0 : render();
233 0 : };
234 :
235 0 : const setErrorIfNotConnected = function() {
236 0 : if (dataStore.connecting === null)
237 0 : return;
238 0 : dataStore.error = _("Not connected");
239 0 : render();
240 0 : };
241 :
242 2 : dataStore.connectionTimeout = 5000;
243 :
244 : // try to connect
245 2 : dataStore.tryConnect = function() {
246 2 : if (dataStore.connecting === null) {
247 2 : dataStore.connecting = window.setTimeout(setErrorIfNotConnected, dataStore.connectionTimeout);
248 2 : render();
249 : // initialize our setroubleshootd client
250 2 : dataStore.client.init()
251 2 : .then(() => {
252 2 : dataStore.connected = true;
253 2 : window.clearTimeout(dataStore.connecting);
254 2 : dataStore.connecting = null;
255 2 : dataStore.error = null; // reset "not connected"
256 2 : render();
257 : // now register a callback to get new entries and get all existing ones
258 : // the order is important, since we don't want to miss an entry
259 2 : dataStore.client.handleAlert(dataStore.handleAlert);
260 2 : dataStore.client.getAlerts()
261 2 : .then(handleMultipleMessages)
262 0 : .catch(() => {
263 0 : console.error("Unable to get setroubleshootd messages");
264 0 : setDisconnected();
265 0 : });
266 2 : })
267 0 : .catch(() => {
268 0 : dataStore.connected = false;
269 0 : window.clearTimeout(dataStore.connecting);
270 0 : dataStore.connecting = null;
271 0 : render();
272 0 : });
273 2 : }
274 2 : };
275 :
276 : // render once initially
277 2 : render();
278 :
279 : // try to connect immediately
280 2 : dataStore.tryConnect();
281 :
282 2 : return dataStore;
283 2 : };
284 :
285 2 : document.addEventListener("DOMContentLoaded", function() {
286 2 : initStore(document.getElementById('app'));
287 2 : });
|